Два парадокса в программах на языке C

от автора

Хочу рассказать о двух странностях, с которыми мне пришлось столкнуться, программируя вычислительные алгоритмы на языке C.

Итак, первое неожиданное поведение для некоторых программистов. Вот маленькая прога.

#include <stdio.h> int main() {     unsigned char a = 1, b;     b = ~a >> 1;     printf("%u\n", 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("%u\n", 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("%d\n", 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 — все считать так, как написано; 1float на самом деле считать в 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.

ссылка на оригинал статьи http://habrahabr.ru/post/201868/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *