Pull to refresh

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

Reading time3 min
Views30K
Хочу рассказать о двух странностях, с которыми мне пришлось столкнуться, программируя вычислительные алгоритмы на языке 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.
Tags:
Hubs:
+11
Comments84

Articles

Change theme settings