gcc -O оптимизация: Помогите мне понять эффект


4

Итак, я изучаю C, и сейчас я просматриваю компьютерные системы: Перспектива программиста 3-го издания и связанные с ним лаборатории. Я сейчас занимаюсь первой лабораторией, для которой я должен реализовать (и таким образом реализовал) следующую функцию.

/* 
* fitsBits - return 1 if x can be represented as an 
* n-bit, two's complement integer. 
* 1 <= n <= 32 
* Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1 
* Legal ops: ! ~ &^| + << >> 
* Max ops: 15 
* Rating: 2 
*/ 
int fitsBits(int x, int n) { 
    int sign_bit = (x >> 31 & 1); 
    int minus_one = ~1+1; 
    int n_minus_one = n + minus_one; 
    return (!(x >> n_minus_one) & !sign_bit) 
      | (!(~(x >> n_minus_one)) & sign_bit); 
} 

Эта функция будет запускать _a_lot_ тестовых таблиц в отношении следующей тестовой функции.

int test_fitsBits(int x, int n) 
{ 
    int TMin_n = -(1 << (n-1)); 
    int TMax_n = (1 << (n-1)) - 1; 
    return x >= TMin_n && x <= TMax_n; 
} 

Теперь, вот, где странные вещи происходят: по умолчанию коды скомпилированы со следующими флагами -O -Wall -m32

Идущих мой код на тестовой функцию дает следующее утверждение FAIL: ОШИБКА: Test fitsBits (-2147483648 [0x80000000], 32 [0x20]) не удалось ... ... Дает 1 [0x1]. Должно быть 0 [0x0]

Кажется, что мой код верен, а тестовый код является фиктивным. При дальнейшем исследовании, кажется, что test_function производит следующие промежуточные результаты:

> Tmin:-2147483648 
> TMax_n:2147483647 
> x: -2147483648 
> x >= TMin_n: 1 
> x <= TMax_n: 0 
> result: 0 

Очевидно -2147483648 < = 2147483647, но сравнение почему-то производит 0.

Если я скомпилировать эту программу без флага -O, все тесты проходят успешно. Может ли кто-нибудь пролить свет на это поведение, пожалуйста?

EDIT: К сожалению, ассемблерный код ужасен макет, не знаю точно, как исправить быстро

Assembly Code without -O; 

.section __TEXT,__text,regular,pure_instructions 
.macosx_version_min 10, 11 
.globl _test_fitsBits 
.align 4, 0x90 
_test_fitsBits:       ## @test_fitsBits 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp0: 
.cfi_def_cfa_offset 16 
Ltmp1: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp2: 
.cfi_def_cfa_register %rbp 
xorl %eax, %eax 
movb %al, %cl 
movl $1, %eax 
xorl %edx, %edx 
movl %edi, -4(%rbp) 
movl %esi, -8(%rbp) 
movl -8(%rbp), %esi 
subl $1, %esi 
movb %cl, -17(%rbp)   ## 1-byte Spill 
movl %esi, %ecx 
            ## kill: CL<def> ECX<kill> 
movl %eax, %esi 
shll %cl, %esi 
subl %esi, %edx 
movl %edx, -12(%rbp) 
movl -8(%rbp), %edx 
subl $1, %edx 
movl %edx, %ecx 
            ## kill: CL<def> ECX<kill> 
shll %cl, %eax 
subl $1, %eax 
movl %eax, -16(%rbp) 
movl -4(%rbp), %eax 
cmpl -12(%rbp), %eax 
movb -17(%rbp), %cl   ## 1-byte Reload 
movb %cl, -18(%rbp)   ## 1-byte Spill 
jl LBB0_2 
## BB#1: 
movl -4(%rbp), %eax 
cmpl -16(%rbp), %eax 
setle %cl 
movb %cl, -18(%rbp)   ## 1-byte Spill 
LBB0_2: 
movb -18(%rbp), %al   ## 1-byte Reload 
andb $1, %al 
movzbl %al, %eax 
popq %rbp 
retq 
.cfi_endproc 

.globl _main 
.align 4, 0x90 
_main:         ## @main 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp3: 
.cfi_def_cfa_offset 16 
Ltmp4: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp5: 
.cfi_def_cfa_register %rbp 
xorl %eax, %eax 
movl $0, -4(%rbp) 
movl %edi, -8(%rbp) 
movq %rsi, -16(%rbp) 
popq %rbp 
retq 
.cfi_endproc 


.subsections_via_symbols 

ассемблере с -O:

.section __TEXT,__text,regular,pure_instructions 
.macosx_version_min 10, 11 
.globl _test_fitsBits 
.align 4, 0x90 
_test_fitsBits:       ## @test_fitsBits 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp0: 
.cfi_def_cfa_offset 16 
Ltmp1: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp2: 
.cfi_def_cfa_register %rbp 
            ## kill: ESI<def> ESI<kill>  RSI<def> 
leal -1(%rsi), %ecx 
movl $1, %eax 
            ## kill: CL<def> CL<kill> ECX<kill> 
shll %cl, %eax 
movl %eax, %ecx 
negl %ecx 
cmpl %edi, %eax 
setg %al 
cmpl %ecx, %edi 
setge %cl 
andb %al, %cl 
movzbl %cl, %eax 
popq %rbp 
retq 
.cfi_endproc 

.globl _main 
.align 4, 0x90 
_main:         ## @main 
.cfi_startproc 
## BB#0: 
pushq %rbp 
Ltmp3: 
.cfi_def_cfa_offset 16 
Ltmp4: 
.cfi_offset %rbp, -16 
movq %rsp, %rbp 
Ltmp5: 
.cfi_def_cfa_register %rbp 
xorl %eax, %eax 
popq %rbp 
retq 
.cfi_endproc 


.subsections_via_symbols 
+2

Вы можете использовать 'Gcc -S' для вывода кода сборки, а затем разместить код сборки для' test_fitsBits'? 23 ноя. 152015-11-23 21:20:17

+1

Насколько широк 'int' на вашей платформе? Каков диапазон значений для 'n'? Помните, что неопределенное поведение имеет правый операнд левого оператора сдвига отрицательный или больший или равный ширине продвинутого левого операнда. 23 ноя. 152015-11-23 21:29:43

+1

int 32bit, но я на 64-битной платформе. Это значения для INT_MAX и INT_MIN при печати из limits.h: 2147483647 -2147483648. 23 ноя. 152015-11-23 21:33:19

  0

@immibis: код сборки добавлен 23 ноя. 152015-11-23 21:38:23

4

Ответы oauh, почему это неопределенное поведение, но не почему проблема возникает только с -O.

-O заставляет компилятор попытаться найти вещи для оптимизации. В этом случае, похоже, понял, что x <= (1 << (n-1)) - 1 действительно то же самое, что и x < (1 << (n-1)) - так что он смог удалить - 1.

Однако это только эквивалентно старому коду, если 1 << (n-1) не переполняется. Компилятор делает эту оптимизацию в любом случае, потому что результат может быть неправильным, если происходит переполнение - потому что что-либо разрешено, если происходит переполнение, потому что это неопределенное поведение.


7
int TMin_n = -(1 << (n-1)); 
int TMax_n = (1 << (n-1)) - 1; 

Об одном система с 32-битным int двумя приведенными выше выражениями поразрядного сдвига вызывают неопределенное поведение, когда n равно 32. Когда int является 32-битным, тогда 1 << 31 является UB в C, так как 1 << 31 не может быть представлен в int.

  0

Спасибо за ваш ответ. Я видел предупреждение компилятора о переполнении, но я хочу понять, почему флаг -O имеет наблюдаемый эффект. 23 ноя. 152015-11-23 21:30:30

+1

Возможно, стоит отметить, что «неопределенное поведение» действительно означает, что * что-либо может случиться. Поэтому, даже если значение печатается правильно в одной точке, это не означает, что правильное значение используется в вычислениях и т. Д. Когда дело доходит до оптимизации, все ставки отключены. Оптимизатор мог просто подумать: «Что, черт возьми, мы делаем неопределенные вещи здесь, давайте просто закончим как можно быстрее». 23 ноя. 152015-11-23 21:30:36

  0

Отрицание INT_MIN или вычитание 1 из него также не определены, поэтому фиксации сдвига в одиночку недостаточно. 23 ноя. 152015-11-23 21:38:29

  0

@ user2357112, за исключением случаев, когда он исправляет его, используя более широкий тип, например, '1ULL << (n - 1)' и назначение, выполняемое для 'unsigned long long'. 23 ноя. 152015-11-23 21:41:51

+1

@ MathieuBorderé - это неопределенное поведение, и компилятор использует два подхода в зависимости от используемой опции оптимизации, которая является обычной, когда существует неопределенное поведение. 23 ноя. 152015-11-23 21:44:23

+1

@ MathieuBorderé в основном без опции оптимизации, когда компилятор находится в режиме «-O0», и обычно обертывается для переполнения, добавляет '-fwrapv' (это заставляет обертывать по подписанному арифметическому переполнению) на' -O', и вы можете получить то же самое результат с параметром '-O0' (или без оптимизации). 23 ноя. 152015-11-23 21:47:25

  0

Кстати, что такое разумная реализация 'fitBits', избегая неопределенного поведения и охватывая весь полный целочисленный диапазон? О лучшем, что я могу придумать, это 8-операция 'unsigned int k = 1U << n - 1; return ((x + k) & ~ (k << 1) + 1); '. 23 ноя. 152015-11-23 22:32:20

  0

принудительное обертывание действительно «исправляет» проблему, спасибо за понимание @ouah 23 ноя. 152015-11-23 22:41:38


1

As pointed out by @ouah, ваш код - undefined behavior. Вместо того, чтобы делать сомнительные ошибки, C обычно говорит, что их поведение «неопределено». Это означает, что компилятор может делать все, что захочет.Большинство компиляторов попытаются сделать то, что вы имеете в виду, но другие интерпретировали «неопределенное поведение» более либерально и decided you really wanted to play a game of Rogue.

-O сообщает компилятору потратить некоторое дополнительное время, пытаясь оптимизировать ваш код. Это изменяет, как компилятор преобразует ваш код C в машинный код. Это будет делать меньше предположений о коде, чтобы попытаться преобразовать его в более эффективную, но эквивалентную версию. Любое неопределенное поведение, которое раньше срабатывало, теперь может сломаться. И наоборот, ваш код может работать нормально с -O и прерываться, когда вы его отключите. Или -g может сломать его. Или запустите свой код в отладчике. Или добавьте printf. Или сильный ветер.

Эти виды ошибок, которые появляются и исчезают, имеют имя, heisenbugs после принципа неопределенности Гейзенберга, в котором говорится, что наблюдение за системой меняет систему.

Вот почему важно включить все предупреждающие флаги вашего компилятора (-Wall - это еще не все, clang имеет -Weverything) и устранить их все. Еще один инструмент, который помогает поймать такие проблемы памяти, - Valgrind.


0

Изменение test.c файла:

int test_fitsBits(int x, int n) { 
    int TMin_n, TMax_n; 
    if (n < 32) { 
     TMin_n = -(1 << (n-1)); 
     TMax_n = (1 << (n-1)) - 1; 
    } else { 
     TMin_n = 0x80000000; 
     TMax_n = ~TMin_n; 
    } 
     return x >= TMin_n && x <= TMax_n; 
}