Хочу рассказать о двух странностях, с которыми мне пришлось столкнуться, программируя вычислительные алгоритмы на языке C.
Итак, первое неожиданное поведение для некоторых программистов. Вот маленькая прога.
#include <stdio.h>
int main()
{
unsigned char a = 1, b;
b = ~a >> 1;
printf("%un", b);
return 0;
}
Разберем ее. Поразрядная операция ~ инвертирует состояние каждого бита байта a, в который изначально записана единица. В результате должны получить 11111110b, то есть 254. Сдвигая этот байт вправо на один бит, должны получить 127. Однако код, который дает, например, компилятор gcc, выводит в консоль число 255?!
Сначала я подумал о том, что дело в приоритете — вдруг у компилятора приоритет операций «косячит»? То есть будто бы сначала делается сдвиг, а потом — инверсия (а что, логично...). Так в чем же дело?
После некоторых раздумываний мне пришла в голову другая гипотеза о том, что при инверсии байт приводится к слову (ну, или к двойному слову), а потом уже инвертированное слово сдвигается. Вот откуда и получается 255 — старшие биты в слове нули, инвертируя их, имеем единицы. Затем, делая сдвиг слова вправо на один бит, в его младшем байте во всех битах будут находиться единицы.
Это и подтверждает следующий код.
#include <stdio.h>
int main()
{
unsigned char a = 1, b;
b = (unsigned char)~a >> 1;
printf("%un", b);
return 0;
}
Теперь мы получаем правильный результат. Но окончательно убедился в этом, дизассемблировав ELF-файл, который дает gcc. Приведу фрагмент полученного ассемблерного кода.
mov [ebp+var_6], 1
movzx eax, [ebp+var_6]
not eax
sar eax, 1
mov [ebp+var_5], al
Сначала через стек единица попадает в 32-х разрядный регистр eax. Далее он инвертируется, а потом сдвигается. Результат достается из младшей части регистра ax — регистра al. Это и оправдывает мою гипотезу — единицы, которые были за нужным байтом, при сдвиге двойного слова в него попали.
Как потом выяснилось, эта ситуация называется Integer Promotion и описывается в п. 6.3.1.1 стандарта C99. Загрузить его можно отсюда www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
Второе неожиданное поведение для некоторых программистов связано с вещественными числами. Имеем следующий код.
#include <stdio.h>
int main()
{
float a = 1.005, b = 1000;
int c = a*b;
printf("%dn", c);
return 0;
}
Компилируя его gcc 4.1.1, получаю 1004. Опять вопрос — откуда берется странный результат? Даже это
int c = (float)(a*b);
также не дает правильного результата.
Полазив по стандарту C89, оказалось, что он ничего не регламентировал о способах работы с вещественными числами. Ведь, когда появилось расширение SSE, компиляторы начали считать смешанным образом — как посчитается быстрее: что-то на FPU, что-то на SSE. В новом стандарте C99 появилась некоторая определенность. Компилятор должен выставить значение макроса FLT_EVAL_METHOD (заголовочный файл float.h) в 0, 1, 2 для способа, которым он считает. Итак, 0 — все считать так, как написано; 1 — float на самом деле считать в double и затем конвертировать обратно во float; 2 — все считать в long double, конвертируя во float или double в конце вычислений соответственно.
Теперь, чтобы заставить считать прогу так, как надо, нужно собирать ее
gcc proga.c -msse
Только после этого у меня в консоль вывелось число 1005. При этом выяснилось, что моя версия компилятора gcc не поддерживает макрос FLT_EVAL_METHOD. Кстати, с double gcc даёт код, выводящий 1004. Только Intel C 9.0 сделал нормальный код с double, но когда я записал
int c = (float)(a*b);
(здесь a и b уже типа double). Без приведения типа код и там даёт 1004.
Автор: pchelintsev_an