Сопоставление кода компилятора

Итак, все началось здесь: Беззнаковое целое и без знака char с тем же значением, но по-другому, почему?

Я написал следующее приложение, чтобы понять, что происходит за кулисами (например, как компилятор справляется с этой проблемой).

#include <stdio.h>
int main()
{
 {
 unsigned char k=-1;
 if(k==-1)
 {
 puts("uc ok\n");
 }
 }
 {
 unsigned int k=-1;
 if(k==-1)
 {
 puts("ui ok");
 }
 }
}
</stdio.h>

И при компиляции с помощью GCC:

gcc -O0 -S -masm=intel h.c

Я получаю следующий файл сборки:

.file "h.c"
 .intel_syntax noprefix
 .section .rodata
.LC0:
 .string "ui ok"
 .text
 .globl main
 .type main, @function
main:
.LFB0:
 .cfi_startproc
 push rbp
 .cfi_def_cfa_offset 16
 .cfi_offset 6, -16
 mov rbp, rsp
 .cfi_def_cfa_register 6
 sub rsp, 16
 mov BYTE PTR [rbp-1], -1
 mov DWORD PTR [rbp-8], -1
 cmp DWORD PTR [rbp-8], -1
 jne .L3
 mov edi, OFFSET FLAT:.LC0
 call puts
.L3:
 leave
 .cfi_def_cfa 7, 8
 ret
 .cfi_endproc
.LFE0:
 .size main, .-main
 .ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
 .section .note.GNU-stack,"",@progbits

И к моему большому удивлению первая проверка НЕ ​​ДАЖЕ.

Но, если я скомпилирую то же самое с Microsoft Visual С++ (2010), я получаю (я отрезал много мусора из этого листинга, почему он не так достоверен):

00B81780 push *** 
00B81781 mov ***,esp 
00B81783 sub esp,0D8h 
00B81789 push *** 
00B8178A push esi 
00B8178B push edi 
00B8178C lea edi,[***-0D8h] 
00B81792 mov ecx,36h 
00B81797 mov eax,0CCCCCCCCh 
00B8179C rep stos dword ptr es:[edi] 
00B8179E mov byte ptr [k],0FFh 
00B817A2 movzx eax,byte ptr [k] 
00B817A6 cmp eax,0FFFFFFFFh 
00B817A9 jne wmain+42h (0B817C2h) 
00B817AB mov esi,esp 
00B817AD push offset string "uc ok\n" (0B857A8h) 
00B817B2 call dword ptr [__imp__puts (0B882ACh)] 
00B817B8 add esp,4 
00B817BB cmp esi,esp 
00B817BD call @ILT+435(__RTC_CheckEsp) (0B811B8h) 
00B817C2 mov dword ptr [k],0FFFFFFFFh 
00B817C9 cmp dword ptr [k],0FFFFFFFFh 
00B817CD jne wmain+66h (0B817E6h) 
00B817CF mov esi,esp 
00B817D1 push offset string "ui ok" (0B857A0h) 
00B817D6 call dword ptr [__imp__puts (0B882ACh)] 
00B817DC add esp,4 
00B817DF cmp esi,esp 
00B817E1 call @ILT+435(__RTC_CheckEsp) (0B811B8h)

Вопрос: почему это происходит? Почему GCC "пропускает" первый IF и как я могу заставить GCC не пропускать его? Оптимизации отключены, но, похоже, он все еще что-то оптимизирует...

4 ответа

Мое предположение (я не разработчик GCC) состоит в том, что он делает достаточно статического анализа, чтобы доказать себе, что первый тест if никогда не верен.

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

Просто для любопытства попробуйте сделать переменную static и/или volatile, чтобы увидеть, что-то изменилось.


Это похоже на проблему с GCC, хотя, по общему признанию, очень незначительную.

Из веб-сайт документации GCC (выделено мной):

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

Таким образом, с помощью -O0 вы можете разместить точку останова между unsigned char k=-1; и if(k==-1), во время этой точки останова изменить k и ожидать, что ветвь будет занята; но это невозможно с испускаемым кодом.


Обновлено: Я предполагаю, что char, как тип под базовым (int) типом, просто масштабируется до целочисленного типа для сравнения. (предполагая, что компилятор взял литерал как целое число и обычно предпочитает целое число с размером слова по размеру байта)

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

Вы можете попытаться заставить литерал быть байтом (листинг или суффиксы), например. сравнивая с ((без знака char) (- 1)), и, возможно, тогда компилятор будет вставлять 1-байтовое сравнение, и результат может быть другим.


Здесь есть несколько мелких пунктов:

  • Компилятор даже не должен смотреть на инициализацию k, чтобы доказать, что условие k == - 1 никогда не может быть истинным в случае без знака char. Дело в том, что значение unsigned 8 бит должно быть увеличено до 32 бит, поскольку правая часть сравнения является целочисленной константой, которая по умолчанию равна 32 битам. Поскольку k без знака, результат этой акции будет 00000000 00000000 00000000 xxxxxxxx. Константа -1 имеет битовый шаблон 11111111 11111111 11111111 11111111, поэтому не имеет значения, что xxxxxxxx, результат сравнения всегда будет ложным.
  • Я могу ошибаться в этом вопросе, но я считаю, что даже если k было указано как volatile, компилятор должен был только загрузить его в регистр (поскольку операция загрузки может вызвать какой-то желательный побочный эффект в аппаратном обеспечении), чтобы не выполнять сравнение или создавать код для недостижимого if-блока.
  • Собственно, опускание сборки сборки для недостижимого кода полностью соответствует цели -O0 для ускорения процесса компиляции.
  • AFAIK, сравнение беззнаковой и отрицательной констант в любом случае равно undefined. По крайней мере, просто нет машинной инструкции для правильной обработки дела, и компиляторы не будут вставлять необходимый код, чтобы обрабатывать его в программном обеспечении, как вы можете видеть из разборки. Все, что вы получаете, - это неявное литье между подписанным и беззнаковым, что приводит к переполнению целочисленного значения (которое само по себе является undefined) и сравнению несмешанного знака.

licensed under cc by-sa 3.0 with attribution.