С++, Как оптимизировать арифметические операции с плавающей запятой?

Я наблюдал удивительное поведение при тестировании простых арифметических операций в предельных случаях на архитектуре 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