Этот вопрос пришел в голову при чтении блестящего ответа Mysticial на вопрос: почему обрабатывать отсортированный массив быстрее, чем несортированный ?
Контекст для задействованных типов:
const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;
В своем ответе он объясняет, что компилятор Intel (ICC) оптимизирует это:
for (int i = 0; i < 100000; ++i)
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += data[c];
... во что-то эквивалентное этому:
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
for (int i = 0; i < 100000; ++i)
sum += data[c];
Оптимизатор распознает, что они эквивалентны, и поэтому меняет циклы , перемещая ветвь за пределы внутреннего цикла. Очень умный!
Но почему он этого не делает?
for (int c = 0; c < arraySize; ++c)
if (data[c] >= 128)
sum += 100000 * data[c];
Надеюсь, Mysticial (или кто-то еще) может дать столь же блестящий ответ. Я никогда раньше не слышал об оптимизации, обсуждаемой в этом другом вопросе, поэтому я очень благодарен за это.
c
performance
compiler-optimization
jhabbott
источник
источник
volatile
, то обмен циклом также был бы недопустимой оптимизацией.Ответы:
Компилятор не может преобразовать
в
потому что последнее может привести к переполнению целых чисел со знаком, тогда как первое - нет. Даже с гарантированным поведением переноса для переполнения подписанных двух целых дополнений, это изменит результат (если
data[c]
это 30000, продукт станет-1294967296
для типичных 32-битныхint
s с переносом, а 100000 раз, добавив 30000 к,sum
будет, если это не переполняется, увеличиваетсяsum
на 3000000000). Обратите внимание, что то же самое верно и для беззнаковых величин с разными числами, переполнение100000 * data[c]
обычно приводит к уменьшению по модулю,2^32
которое не должно появляться в конечном результате.Он мог бы превратить его в
хотя, если, как обычно,
long long
достаточно больше, чемint
.Почему он этого не делает, я не могу сказать, я предполагаю, что это то, что сказал Mysticial: «по-видимому, он не запускает проход с разрушением цикла после обмена циклом».
Обратите внимание, что сам обмен циклами обычно недопустим (для целых чисел со знаком), поскольку
может привести к переполнению, где
не будет. Здесь кошерно, так как условие гарантирует,
data[c]
что все, что добавлено, имеют один и тот же знак, поэтому, если одно переполняется, то оба.Я бы не был слишком уверен, что компилятор учел это (@Mysticial, не могли бы вы попробовать с таким условием,
data[c] & 0x80
которое может быть истинным для положительных и отрицательных значений?). У меня были компиляторы, которые выполняли недопустимую оптимизацию (например, пару лет назад у меня было ICC (11.0, iirc), использующее преобразование signed-32-bit-int-to-double,1.0/n
гдеn
было anunsigned int
. Было примерно в два раза быстрее, чем у gcc вывод. Но не так, многие значения были больше2^31
, упс.).источник
ADD.W A6,$A000
, забывая о том, что операции со словом с адресными регистрами расширяют знак до 32 бит перед добавлением. Потребовалось некоторое время, чтобы устранить неполадки, поскольку единственное, что код делал между этимADD
и в следующий раз, когда он извлекал A6 из стека, - это восстанавливать регистры вызывающего абонента, которые он сохранил в этом кадре ...MyArray[0] = 4;
я мог проверить адресMyArray
и посмотреть на это место до и после выполнения оператора; это не изменится. Код был чем-то вроде,move.B @A3,#4
и A3 должен был всегда указывать наMyArray
любое время выполнения этой инструкции, но это не так. Весело.Этот ответ не относится к конкретному случаю, на который указывает ссылка, но он применим к заголовку вопроса и может быть интересен будущим читателям:
Из-за конечной точности повторное сложение с плавающей запятой не эквивалентно умножению . Рассматривать:
демонстрация
источник
Компилятор содержит различные проходы, выполняющие оптимизацию. Обычно в каждом проходе выполняется либо оптимизация операторов, либо оптимизация цикла. В настоящее время не существует модели, которая оптимизирует тело цикла на основе заголовков цикла. Это трудно обнаружить и встречается реже.
Оптимизация, которая была сделана, была движением кода с инвариантным циклом. Это можно сделать с помощью набора приемов.
источник
Что ж, я предполагаю, что некоторые компиляторы могут выполнять такую оптимизацию, предполагая, что мы говорим о целочисленной арифметике.
В то же время некоторые компиляторы могут отказаться делать это, потому что замена повторяющегося сложения умножением может изменить поведение кода при переполнении. Для беззнаковых целочисленных типов это не должно иметь значения, поскольку их поведение при переполнении полностью определяется языком. Но для подписанных это может (возможно, не на платформе дополнения 2). Верно, что подписанное переполнение на самом деле приводит к неопределенному поведению в C, а это означает, что должно быть совершенно нормально игнорировать эту семантику переполнения в целом, но не все компиляторы достаточно храбры, чтобы сделать это. Он часто вызывает много критики со стороны толпы «C - это просто язык ассемблера более высокого уровня». (Помните, что произошло, когда GCC ввел оптимизацию на основе семантики строгого псевдонима?)
Исторически GCC зарекомендовал себя как компилятор, у которого есть все, что нужно для таких решительных шагов, но другие компиляторы могут предпочесть придерживаться воспринимаемого «пользовательского» поведения, даже если оно не определено языком.
источник
Теперь это так - по крайней мере, clang :
компилируется с -O1 в
Целочисленное переполнение тут ни при чем; если есть целочисленное переполнение, вызывающее неопределенное поведение, это может произойти в любом случае. Вот такая же функция, использующая
int
вместоlong
:компилируется с -O1 в
источник
На пути к такой оптимизации есть концептуальный барьер. Авторы компилятора прилагают много усилий для уменьшения силы - например, заменяя умножение сложением и сдвигом. Они привыкли думать, что размножение - это плохо. Так что случай, когда нужно пойти другим путем, удивителен и противоречит здравому смыслу. Так что никто не думает реализовывать это.
источник
Люди, которые разрабатывают и обслуживают компиляторы, имеют ограниченное количество времени и энергии, которые они могут тратить на свою работу, поэтому они обычно хотят сосредоточиться на том, что больше всего волнует их пользователей: превращении хорошо написанного кода в быстрый код. Они не хотят тратить свое время, пытаясь найти способы превратить глупый код в быстрый код - для этого и нужна проверка кода. В языке высокого уровня может быть «глупый» код, который выражает важную идею, поэтому разработчикам стоит потратить время на то, чтобы сделать это быстро - например, сокращенная вырубка леса и слияние потоков позволяют программам Haskell структурироваться вокруг определенных типов ленивых создаваемые структуры данных для компиляции в жесткие циклы, не выделяющие память. Но такой стимул просто не применим к превращению зацикленного сложения в умножение. Если вы хотите, чтобы это было быстро,
источник