С++, Как оптимизировать арифметические операции с плавающей запятой?
Я наблюдал удивительное поведение при тестировании простых арифметических операций в предельных случаях на архитектуре x86:
const double max = 9.9e307; // Near std::numeric_limits<double>::max()
const double init[] = { max, max, max };
const valarray<double> myvalarray(init, 3);
const double mysum = myvalarray.sum();
cout << "Sum is " << mysum << endl; // Sum is 1.#INF
const double myavg1 = mysum/myvalarray.size();
cout << "Average (1) is " << myavg1 << endl; // Average (1) is 1.#INF
const double myavg2 = myvalarray.sum()/myvalarray.size();
cout << "Average (2) is " << myavg2 << endl; // Average (2) is 9.9e+307
(Протестировано с MSVC в режиме выпуска, а также с gcc через Codepad.org. Режим отладки MSVC устанавливает среднее значение (2) в #INF
.)
Я ожидал, что средний (2) будет равен среднему (1), но мне кажется, что встроенный оператор разделения С++ получил оптимизацию от компилятора и каким-то образом помешал накоплению достичь #INF
.
Короче: среднее значение больших чисел не дает #INF
.
Я наблюдал такое же поведение с std-алгоритмом на MSVC:
const double mysum = accumulate(init, init+3, 0.);
cout << "Sum is " << mysum << endl; // Sum is 1.#INF
const double myavg1 = mysum/static_cast<size_t>(3);
cout << "Average (1) is " << myavg1 << endl; // Average (1) is 1.#INF
const double myavg2 = accumulate(init, init+3, 0.)/static_cast<size_t>(3);
cout << "Average (2) is " << myavg2 << endl; // Average (2) is 9.9e+307
(На этот раз gcc устанавливает среднее значение (2) на #INF
: http://codepad.org/C5CTEYHj.)
- Кто-нибудь может объяснить, как этот "эффект" был достигнут?
- Это функция? Или я могу считать это "неожиданным поведением" вместо просто "удивительного"?
Спасибо
Ответы
Ответ 1
Просто предположим, но: может быть, что Average (2) вычисляется непосредственно в регистры с плавающей запятой, которые имеют ширину 80 бит и переполняют позже, чем 64-битное хранилище для удваивания в памяти. Вы должны проверить разборку вашего кода, чтобы убедиться, что это действительно так.
Ответ 2
Это своего рода функция, или, по крайней мере, намеренная.
В основном, регистры с плавающей запятой на x86 имеют больше
точность и дальность, чем двойной (15-битный показатель, а не
11, 64 бит матисса, а не 52). Стандарт С++ позволяет
используя более высокую точность и диапазон для промежуточных значений, и
почти любой компилятор для Intel сделает это в некоторых
обстоятельства; разница в производительности значительна.
Получаете ли вы расширенную точность или нет, зависит от того, когда
и компилятор проливает память. (Сохранение результата в
именованная переменная требует, чтобы компилятор преобразовал ее в фактическую
двойной точности, по крайней мере, в соответствии со стандартом.)
худший случай этого я видел, был какой-то код, который в основном делал:
return p1->average() < p2->average()
average()
делает то, что вы ожидаете от внутренней таблицы
в данных. В некоторых случаях p1
и p2
на самом деле указывают
к тому же элементу, но возвращаемое значение все равно будет истинным;
результаты одного из вызовов функций будут
памяти (и усечен до double
), результаты другой
остался в регистре с плавающей запятой.
(Функция использовалась как функция упорядочения для sort
,
и полученный код разбился, поскольку из-за этого эффекта он
не определили достаточно строгие критерии упорядочения, а
sort
, если вне диапазона, переданного ему.)
Ответ 3
Бывают ситуации, когда компилятор может использовать более широкий тип, чем тот, который подразумевается объявленным типом, но AFAIK, это не один из них.
Я думаю, что мы имеем эффект, подобный эффекту Gcc bug 323, где дополнительная точность используется, когда это не должно быть.
x86 имеет 80 битных внутренних регистров FP. В то время как gcc имеет тенденцию использовать их с максимальной точностью (таким образом, ошибка 323), я понимаю, что MSVC задает точность до 53 бит, причем один из 64 бит удваивается. Но удлиненное значение не является единственной разницей в 80 бит FP, диапазон экспоненты также увеличивается. И IIRC, в x86 нет настроек, заставляющих использовать диапазон из 64 бит в два раза.
теперь кажется, что код недоступен, или я бы проверил ваш код на архитектуре без двукратного бита длиной 80 бит.
Ответ 4
g++ -O0 -g -S test.cpp -o test.s0
g++ -O3 -g -S test.cpp -o test.s3
Сравнение test.s [03] показывает, что действительно valarray:: sum даже не называется снова. Я не смотрел на него долго, но следующие фрагменты, по-видимому, являются определяющими фрагментами:
.loc 3 16 0 ; test.s0
leal -72(%ebp), %eax
movl %eax, (%esp)
call __ZNKSt8valarrayIdE3sumEv
fstpl -96(%ebp)
leal -72(%ebp), %eax
movl %eax, (%esp)
call __ZNKSt8valarrayIdE4sizeEv
movl $0, %edx
pushl %edx
pushl %eax
fildq (%esp)
leal 8(%esp), %esp
fdivrl -96(%ebp)
fstpl -24(%ebp)
.loc 3 17 0
против
.loc 1 16 0 ; test.s3
faddl 16(%eax)
fdivs LC3
fstpl -336(%ebp)
LVL6:
LBB449:
LBB450:
.loc 4 514 0