Оптимизация компилятора при маркировке int без знака? - PullRequest
1 голос
/ 03 февраля 2011

Для целого числа, которое никогда не будет принимать значения -ve, можно использовать unsigned int или int.Есть ли разница в x86_64 с точки зрения компилятора или чисто с точки зрения цикла процессора?

Ответы [ 5 ]

11 голосов
/ 03 февраля 2011

Это зависит. Это может произойти в любом случае, в зависимости от того, что вы делаете с этим int, а также от свойств базового оборудования.


Очевидным примером в пользу unsigned int является целочисленная операция деления. В C / C ++ целочисленное деление должно округлять до нуля , тогда как машинное целочисленное деление на x86 округляет в сторону отрицательной бесконечности. Кроме того, различные «оптимизированные» замены для целочисленного деления (сдвиги и т. Д.) Также обычно округляются к отрицательной бесконечности. Таким образом, чтобы удовлетворить стандартные требования, компилятор вынужден корректировать подписанные результаты целочисленного деления с помощью дополнительных машинных инструкций. В случае целочисленного деления без знака эта проблема не возникает, поэтому, как правило, целочисленное деление работает гораздо быстрее для типов без знака, чем для типов со знаком.

Например, рассмотрим это простое выражение

rand() / 2

Код, сгенерированный для этого выражения компилятором MSVC, обычно будет выглядеть следующим образом

call        rand
cdq              
sub         eax,edx 
sar         eax,1 

Обратите внимание, что вместо одной инструкции сдвига (sar) мы видим здесь целый набор инструкций, т. Е. Нашему sar предшествуют две дополнительные инструкции (cdq и sub). Эти дополнительные инструкции предназначены только для «корректировки» деления, чтобы заставить его генерировать «правильный» (с точки зрения языка Си) результат. Обратите внимание, что компилятор не знает, что ваше значение всегда будет положительным, поэтому он должен генерировать эти инструкции всегда, безоговорочно. Они никогда не будут делать ничего полезного, тратя таким образом время процессора.

Не смотрите на код для

(unsigned) rand() / 2

Это просто

call        rand  
shr         eax,1 

В этом случае один сдвиг добился цели, предоставив нам астрономически более быстрый код (только для деления).


С другой стороны, когда вы смешиваете целочисленную арифметику и арифметику с плавающей точкой FPU, целочисленные типы со знаком могут работать быстрее, поскольку набор инструкций FPU содержит непосредственную инструкцию для загрузки / хранения целочисленных значений со знаком, но не имеет инструкций для целого числа без знака значения.

Чтобы проиллюстрировать это, можно использовать следующую простую функцию

double zero() { return rand(); }

Сгенерированный код, как правило, будет очень простым

call        rand 
mov         dword ptr [esp],eax 
fild        dword ptr [esp]

Но если мы изменим нашу функцию на

double zero() { return (unsigned) rand(); }

сгенерированный код изменится на

call        rand
test        eax,eax 
mov         dword ptr [esp],eax 
fild        dword ptr [esp] 
jge         zero+17h 
fadd        qword ptr [__real@41f0000000000000 (4020F8h)] 

Этот код заметно больше, потому что набор инструкций FPU не работает с целыми типами без знака, поэтому после загрузки значения без знака необходимы дополнительные корректировки (что и делает это условное fadd).


Существуют и другие контексты и примеры, которые можно использовать для демонстрации того, что это работает в любом случае. Итак, опять все зависит. Но, как правило, все это не будет иметь значения для общей картины эффективности вашей программы. Я обычно предпочитаю использовать типы без знака для представления количества без знака. В моем коде 99% целочисленных типов без знака. Но я делаю это из чисто концептуальных соображений, а не из-за увеличения производительности.

2 голосов
/ 03 февраля 2011

Это почти наверняка не будет иметь значения, но иногда компилятор может играть в игры со знаком типов, чтобы сбрить пару циклов, но, честно говоря, это, вероятно, незначительное изменение в целом.

Например, предположим, у вас есть int x и вы хотите написать:

if(x >= 10 && x < 200) { /* ... */ }

Вы (или, что еще лучше, компилятор) можете немного изменить это, чтобы сделать еще одно сравнение:

if((unsigned int)(x - 10) < 190) { /* ... */ }

Предполагается, что int представлен в комплименте 2, так что если (x - 10) меньше, чем 0, становится огромным значением при просмотре как unsigned int. Например, в типичной системе x86 (unsigned int)-1 == 0xffffffff явно больше тестируемого 190.

В лучшем случае это микрооптимизация, и лучше оставить ее без компилятора, вместо этого вы должны написать код, который выражает то, что вы имеете в виду, и, если он слишком медленный, профилировать и решить, где на самом деле необходимо разобраться.

2 голосов
/ 03 февраля 2011

Подписанные типы по своей природе более оптимизируемы в большинстве случаев, потому что компилятор может игнорировать возможность переполнения и упрощать / переупорядочивать арифметику любым удобным для него способом. С другой стороны, неподписанные типы по своей природе более безопасны, потому что результат всегда четко определен (даже если не так, как вы наивно полагаете, что это должно быть).

Единственный случай, когда типы без знака лучше оптимизируются, - это когда вы пишете деление / остаток степенью двойки. Для типов без знака это преобразуется непосредственно в битовое смещение, битовое и. Для типов со знаком, если только компилятор не может установить, что значение, как известно, является положительным, он должен сгенерировать дополнительный код, чтобы компенсировать проблему "один на один" с отрицательными числами (согласно C, -3/2 равно -1, тогда как алгебраически и побитовыми операциями это -2).

1 голос
/ 03 февраля 2011

С точки зрения ALU добавление (или что-либо еще) значений со знаком или без знака не имеет значения, поскольку они оба представлены группой битов.0100 + 1011 всегда 1111, но вы выбираете, если это 4 + (-5) = -1 или 4 + 11 = 15.
Так что я согласен с @Mark, вы должны выбрать лучший тип данных, чтобы помочь другимпонять твой код.

1 голос
/ 03 февраля 2011

Я не думаю, что это будет иметь большое значение с точки зрения процессора или компилятора.Один из возможных случаев был бы, если бы он позволил компилятору знать, что число никогда не будет отрицательным, и оптимизировал бы код.

Однако это полезно для человека, читающего ваш код, чтобы они знали домен рассматриваемой переменной.

...