Мне интересно об использовании кода, как следующий
int result = 0;
int factor = 1;
for (...) {
result = ...
factor *= 10;
}
return result;
Если цикл повторяется с течением n
времени, то factor
он умножается на 10
точное n
время. Тем не менее, factor
используется только после умножения на 10
общее количество n-1
раз. Если мы предполагаем, что factor
никогда не переполняется, за исключением последней итерации цикла, но может переполниться на последней итерации цикла, то должен ли такой код быть приемлемым? В этом случае значение factor
наверняка никогда не будет использовано после переполнения.
У меня спор о том, должен ли такой кодекс быть принят. Можно было бы поместить умножение в оператор if и просто не делать умножение на последней итерации цикла, когда он может переполниться. Недостатком является то, что он загромождает код и добавляет ненужную ветвь, которую нужно будет проверять на всех предыдущих итерациях цикла. Я мог бы также выполнить итерации цикла меньше и повторить тело цикла один раз после цикла, опять же, это усложняет код.
Фактический рассматриваемый код используется в узком внутреннем цикле, который потребляет большую часть общего процессорного времени в графическом приложении реального времени.
Ответы:
Компиляторы предполагают, что действительная программа на C ++ не содержит UB. Рассмотрим для примера:
Если
x == nullptr
затем разыменование его и присвоение значения UB. Следовательно, единственный способ, которым это может закончиться в допустимой программе, - это когдаx == nullptr
никогда не будет возвращаться значение true, и компилятор может предположить, что согласно правилу «как будто» вышеприведенное эквивалентноТеперь в вашем коде
Последнее умножение
factor
не может произойти в допустимой программе (переполнение со знаком не определено). Следовательно, назначениеresult
не может произойти. Поскольку до последней итерации нет возможности ветвиться, предыдущей итерации быть не может. В конце концов, часть кода, которая является правильной (т. Е. Никогда не происходит неопределенное поведение):источник
INT_MAX >= 10000000000
с другой функцией, вызываемой в случае, когдаINT_MAX
она меньше.i <= n
циклы всегда бесконечны, какi<n
циклы. Иint i
увеличивайте ширину указателя в цикле вместо того, чтобы повторять знак для возможного переноса индексации массива на первые элементы массива 4G.Поведение
int
переполнения не определено.Неважно, если вы читаете
factor
вне тела цикла; если он переполнен к тому времени, то поведение вашего кода до, после и несколько парадоксально до того, как переполнение не определено.Одна проблема, которая может возникнуть при сохранении этого кода, заключается в том, что компиляторы становятся все более и более агрессивными, когда речь заходит об оптимизации. В частности, они развивают привычку, в которой они предполагают, что неопределенного поведения никогда не бывает. Чтобы это имело место, они могут полностью удалить
for
цикл.Разве вы не можете использовать
unsigned
тип дляfactor
хотя тогда вам нужно беспокоиться о нежелательной конверсииint
вunsigned
в выражениях , содержащих как?источник
factor
«используется» в задании обратно к себе.Может быть полезно рассмотреть реальные оптимизаторы. Разматывание петли - известная техника. Основная идея развертывания в том, что
может быть лучше реализовано за кулисами, как
Это простой случай с фиксированной границей. Но современные компиляторы также могут сделать это для переменных границ:
становится
Очевидно, это работает, только если компилятор знает, что N <= 3. И вот где мы вернемся к первоначальному вопросу. Поскольку компилятор знает, что переполнение со знаком не происходит , он знает, что цикл может выполняться максимум 9 раз на 32-разрядных архитектурах.
10^10 > 2^32
, Следовательно, он может развернуть цикл из 9 итераций. Но предполагаемый максимум был 10 итераций! ,Может случиться так, что вы получите относительный переход к инструкции сборки (9-N) с N = 10, поэтому смещение равно -1, что является самой инструкцией перехода. К сожалению. Это вполне допустимая оптимизация цикла для четко определенного C ++, но приведенный пример превращается в жесткий бесконечный цикл.
источник
Любое целочисленное переполнение со знаком приводит к неопределенному поведению, независимо от того, было ли переполненное значение считано или нет.
Возможно, в вашем случае вы можете вывести первую итерацию из цикла, повернув это
в это
При включенной оптимизации компилятор может развернуть второй цикл выше в один условный переход.
источник
factor *= 10;
Это UB; в терминах ISO C ++ все поведение всей программы совершенно не определено для выполнения, которое в конечном итоге достигает UB. Классический пример, насколько заботится стандарт C ++, может заставить демонов вылететь из вашего носа. (Я рекомендую против использования реализации, где носовые демоны являются реальной возможностью). Смотрите другие ответы для более подробной информации.
Компиляторы могут «вызывать проблемы» во время компиляции для путей выполнения, которые они могут видеть, приводя к UB, видимому во время компиляции, например, предполагая, что эти базовые блоки никогда не достигаются.
Смотрите также, что должен знать каждый программист на C о неопределенном поведении (блог LLVM). Как объяснялось там, UB со знаком переполнения позволяет компиляторам доказывать, что
for(... i <= n ...)
циклы не являются бесконечными циклами даже для неизвестныхn
. Это также позволяет им "продвигать" счетчики цикла int на ширину указателя вместо повторения расширения знака. (Таким образом, последствием UB в этом случае может быть доступ к элементам массива с низкими 64 КБ или 4 ГБ, если вы ожидали завертывания со знакомi
в его диапазон значений.)В некоторых случаях компиляторы будут выдавать недопустимую инструкцию, такую как x86,
ud2
для блока, который вызывает UB, если он когда-либо будет выполнен. (Обратите внимание , что функция может не всегда можно назвать, поэтому составители не могут вообще сходить с ума и нарушать другие функции, или даже возможные пути через функции , которые не попали UB. То есть машинный код он компилирует должно работать для все входы, которые не ведут к UB.)Вероятно, наиболее эффективным решением является очистка вручную последней итерации, чтобы
factor*=10
избежать ненужного .Или, если тело цикла большое, попробуйте просто использовать беззнаковый тип для
factor
. Затем вы можете позволить многократному переполнению без знака, и он просто выполнит четко определенное перенос до некоторой степени 2 (число битов значения в типе без знака).Это хорошо, даже если вы используете его со знаковыми типами, особенно если ваше unsigned-> подписанное преобразование никогда не переполняется.
Преобразование между unsigned и дополнением 2 со знаком является бесплатным (один и тот же битовый шаблон для всех значений); Обтекание по модулю для int -> unsigned, определяемое стандартом C ++, упрощает использование только одного и того же битового шаблона, в отличие от дополнения или знака / величины.
И unsigned-> Sign также тривиален, хотя он определяется реализацией для значений, превышающих
INT_MAX
. Если вы не используете огромный неподписанный результат последней итерации, вам не о чем беспокоиться. Но если да, см. Не определено ли преобразование из неподписанного в подписанное? , Случай «значение не подходит» определяется реализацией , что означает, что реализация должна выбрать какое-то поведение; здравомыслящие просто усекают (если необходимо) битовый шаблон без знака и используют его как подписанный, потому что это работает для значений в диапазоне так же, без дополнительной работы. И это определенно не UB. Таким образом, большие значения без знака могут стать отрицательными знаковыми целыми числами. например, послеint x = u;
gcc и clang не оптимизироватьx>=0
как всегда, правда, даже без-fwrapv
, потому что они определили поведение.источник
Если вы можете допустить несколько дополнительных инструкций по сборке в цикле, вместо
ты можешь написать:
чтобы избежать последнего умножения.
!factor
не введет ветку:Этот код
также приводит к сборке без ответвлений после оптимизации:
(Составлено с GCC 8.3.0
-O3
)источник
factor
немного увеличивает задержку цепочки зависимостей, переносимых циклом . Или нет: когда он компилирует в 2 раза LEA это примерно столь же эффективно , как LEA + ADD делать ,f *= 10
какf*5*2
сtest
задержкой скрыта первымLEA
. Но это требует дополнительных мопов в цикле, так что возможен недостаток пропускной способности (или, по крайней мере, проблема дружелюбия к гиперпоточности)Вы не показали, что в скобках
for
утверждения, но я собираюсь предположить, что это что-то вроде этого:Вы можете просто переместить инкремент счетчика и проверку завершения цикла в тело:
Количество инструкций по сборке в цикле останется прежним.
Вдохновленный презентацией Андрея Александреску "Скорость в умах людей".
источник
Рассмотрим функцию:
Согласно опубликованному Обоснованию, авторы Стандарта ожидали бы, что если бы эта функция была вызвана на (например) обычном 32-битном компьютере с аргументами 0xC000 и 0xC000, продвижение операндов
*
tosigned int
приведет к тому, что вычисления приведут к -0x10000000 , который при преобразовании вunsigned
даст0x90000000u
- тот же ответ, как если бы они сделалиunsigned short
повышениеunsigned
. Тем не менее, gcc иногда оптимизирует эту функцию таким образом, чтобы в случае переполнения он вел себя бессмысленно. Любой код, в котором некоторая комбинация входных данных может вызвать переполнение, должен обрабатываться с-fwrapv
параметром, если только было бы неприемлемо позволить создателям намеренно искаженного ввода выполнять произвольный код по своему выбору.источник
Почему бы не это:
источник
...
тело цикла дляfactor = 1
илиfactor = 10
, только 100 и выше. Вы должны очистить первую итерацию и начать сfactor = 1
того, что хотите, чтобы это работало.Есть много разных лиц Неопределенного Поведения, и то, что приемлемо, зависит от использования.
Само по себе это немного необычно, но как бы то ни было ... если это действительно так, то UB, скорее всего, находится в пределах "допустимого, приемлемого" . Графическое программирование печально известно из-за хаков и безобразных вещей. Пока он «работает» и на его создание не уходит больше 16,6 мс, обычно это никого не волнует. Но все же, имейте в виду, что значит вызывать UB.
Во-первых, это стандарт. С этой точки зрения, нечего обсуждать, и нет способа оправдать, ваш код просто недействителен. Там нет ни «если», ни «когда», просто это не правильный код. С вашей точки зрения, вы могли бы также сказать, что это средний палец вверх, и в любом случае в 95-99% случаев вам будет хорошо.
Далее есть аппаратная сторона. Есть некоторые необычные, странные архитектуры, где это проблема. Я говорю «необычно, странно», потому что на одной архитектуре, которая составляет 80% всех компьютеров (или на двух архитектурах, которые вместе составляют 95% всех компьютеров), переполнение - это «да, все равно, все равно» вещь на аппаратном уровне. Вы уверены, что получите мусор (хотя все еще предсказуемый) результат, но ничего плохого не происходит.
Это нев случае с любой архитектурой вы вполне можете получить ловушку переполнения (хотя, если посмотреть на то, как вы говорите о графическом приложении, шансы оказаться на такой странной архитектуре довольно малы). Является ли переносимость проблемой? Если это так, вы можете воздержаться.
Наконец, есть сторона компилятора / оптимизатора. Одна из причин, по которой переполнение не определено, заключается в том, что просто оставить его таким, как когда-то, было легче всего справиться с оборудованием. Но другая причина заключается в том , что , например ,
x+1
будет гарантированно всегда быть большеx
, и компилятор / оптимизатор может использовать эти знания. Теперь, для ранее упомянутого случая, как известно, компиляторы действительно действуют таким образом и просто удаляют полные блоки (несколько лет назад существовал эксплойт Linux, который был основан на том, что компилятор удалил некоторый код проверки именно из-за этого).В вашем случае я бы серьезно усомнился в том, что компилятор делает какие-то особые, странные оптимизации. Тем не менее, что вы знаете, что я знаю. Если есть сомнения, попробуйте, Если это работает, вам хорошо идти.
(И, наконец, есть, конечно, аудит кода, вам, возможно, придется потратить время на обсуждение этого вопроса с аудитором, если вам не повезло.)
источник