пятница, 4 июля 2014 г.

Не все перемещения на регистр одинаково полезны

Словил забавную багу (а может и не багу) оптимизатора на казалось бы простеньком примере:

$ cat t.c
#include <stdio.h>

typedef double t;

t a = 0.5;
t b = 0.23;
t c = 6.0;

int main (void)
{
  t e, f;

  e = a - b;
  f = e * c;

  printf ("%.30f\n", f);
  return 0;
}

$ gcc t.c -O2 && ./a.out && gcc t.c && ./a.out
1.619999999999999884536805438984
1.620000000000000106581410364015


Такой эффект наблюдается с совершенно бородатых времён (ещё по-моему gcc-2.x такое выдавал). И наблюдается он только на 32-битном x86.

Сначала я думал, что это вина gcc, особенно с учётом того что llvm отрабатывает нормально. Но я эту багу нашёл в багзилле, суть вот в чём. Без оптимизации компилятор хранит double переменные на стеке, там они занимают положенные 64 бита и всё хорошо. А при включённой оптимизации он перемещает значение на плавающий регистр, который имеет размер 80 бит.

На вики есть объяснение почему именно 80 бит. Это связано с тем, что для удвоения точности экспоненту нужно увеличить на 1 бит и получить 12 бит, а мантиссу до 77 вместо старых 55 бит. Решение довольно спорное, т.к. оно не портируемое. Т.е. программе для того чтобы результат в разных режимах на разных машинах выдавал одинаковый результат нужно весьма извращаться. Иногда помогает ключ -ffloat-store, который запрещает хранить плавающие значения на регистрах.

2 комментария:

  1. Я бы не считал это багом gcc, а неприятной особенностью всей floating-point-математики. Есть еще много подводных камней, которые дадут недетерминированности в плавающей арифметике. Например, флаги FPU.

    Можно сделать детерминированную арифметику. Но это будет медленно.

    ОтветитьУдалить
    Ответы
    1. Я бы назвал это неприятной особенностью процессоров intel. Тут вообще говоря проблема вызвана двумя решениями.

      С одной стороны мы имеем желание оптимизирующего компилятора всё что можно вынести на регистр. Это совершенно понятное законное желание. Но на сколько корректно делать вынос на регистр если заранее известно что меняется результат - вопрос открытый. И да, понятно, что это частный случай конкретного процессора, а подпирать костылём такую особенность через компилятор никогда не хочется.

      С другой - intel. По какой-то причине они (не знаю по какой) сделали для двойной точности отдельные регистры другого размера. Не знаю на сколько это было оправдано тогда, но сейчас из-за этого возникают проблемы у других процессоров, которым нужно повторять поведение intel'ов.

      А про то какие огороды нужно городить если софт должен одинаково работать на интеле, спарке и эльбрусе я вообще молчу.

      Удалить