Почему GDB оценивает арифметику с плавающей запятой по-разному от С++? - программирование
Подтвердить что ты не робот

Почему GDB оценивает арифметику с плавающей запятой по-разному от С++?

Я столкнулся с чем-то немного запутанным, пытаясь справиться с арифметической проблемой с плавающей запятой.

Во-первых, код. Я отвлек сущность моей проблемы в этом примере:

#include <iostream>
#include <iomanip>

using namespace std;
typedef union {long long ll; double d;} bindouble;

int main(int argc, char** argv) {
    bindouble y, z, tau, xinum, xiden;
    y.d = 1.0d;
    z.ll = 0x3fc5f8e2f0686eee; // double 0.17165791262311053
    tau.ll = 0x3fab51c5e0bf9ef7; // double 0.053358253178712838
    // xinum = double 0.16249854626123722 (0x3fc4ccc09aeb769a)
    xinum.d = y.d * (z.d - tau.d) - tau.d * (z.d - 1);
    // xiden = double 0.16249854626123725 (0x3fc4ccc09aeb769b)
    xiden.d = z.d * (1 - tau.d);
    cout << hex << xinum.ll << endl << xiden.ll << endl;
}

xinum и xiden должны иметь одинаковое значение (когда y == 1), но из-за ошибки округления с плавающей запятой они этого не делают. Эта часть, которую я получаю.

Вопрос возник, когда я запускал этот код (фактически, свою настоящую программу) через GDB, чтобы отслеживать несоответствие. Если я использую GDB для воспроизведения оценок, выполненных в коде, это дает другой результат для xiden:

$ gdb mathtest
GNU gdb (Gentoo 7.5 p1) 7.5
...
This GDB was configured as "x86_64-pc-linux-gnu".
...
(gdb) break 16
Breakpoint 1 at 0x4008ef: file mathtest.cpp, line 16.
(gdb) run
Starting program: /home/diazona/tmp/mathtest 
...
Breakpoint 1, main (argc=1, argv=0x7fffffffd5f8) at mathtest.cpp:16
16          cout << hex << xinum.ll << endl << xiden.ll << endl;
(gdb) print xiden.d
$1 = 0.16249854626123725
(gdb) print z.d * (1 - tau.d)
$2 = 0.16249854626123722

Вы заметите, что если я попрошу GDB рассчитать z.d * (1 - tau.d), он даст 0.16249854626123722 (0x3fc4ccc09aeb769a), тогда как фактический код С++, который вычисляет то же самое в программе, дает 0.16249854626123725 (0x3fc4ccc09aeb769b). Таким образом, GDB должен использовать другую модель оценки для арифметики с плавающей запятой. Может ли кто-нибудь пролить свет на это? Как оценка GDB отличается от оценки моего процессора?

Я рассмотрел этот связанный вопрос, задав вопрос о том, что GDB оценивает sqrt(3) на 0, но это не должно быть одним и тем же, потому что нет вызовов функций здесь.

4b9b3361

Ответ 1

Может быть потому, что FPU x86 работает в регистрах с точностью до 80 бит, но округляется до 64 бит, когда значение сохраняется в памяти. GDB будет храниться в памяти на каждом шаге (интерпретированного) вычисления.

Ответ 2

Система оценки выражения времени исполнения GDB, конечно же, не гарантирует выполнение того же эффективного машинного кода для операций с плавающей запятой, как оптимизированный и переупорядоченный машинный код, сгенерированный вашим компилятором, для вычисления результата того же символического выражения. В самом деле, гарантированно не выполнять один и тот же машинный код для вычисления значения данного выражения z.d * (1 - tau.d), так как это можно считать подмножеством вашей программы, для которого изолированная оценка выражения выполняется во время выполнения в произвольном, "символическом правильный" способ.

Генерация кода с плавающей запятой и ее реализация процессором особенно подвержены символической несогласованности с другими реализациями (такими как оценщик выражения времени выполнения) из-за оптимизации (подстановка, переупорядочение, устранение подвыражений и т.д.), выбор инструкции, выбор распределения регистров и среду с плавающей запятой. Если ваш фрагмент содержит много автоматических переменных во временных выражениях (как и у вас), генерация кода имеет особенно большую свободу при равных нулю проходах оптимизации, и с этой свободой возникает вероятность - в этом случае - потеря точности в наименее значимый бит, который выглядит непоследовательным.

Вы не узнаете, почему оценщик среды исполнения GDB выполнил все инструкции, которые он сделал, без глубокого понимания исходного кода GDB, настроек сборки и собственного кода, сгенерированного с помощью компиляции.

Вы можете нарисовать на сгенерированной сборке свою процедуру, чтобы понять, как работают последние магазины в z, tau и [в отличие] xiden. Поток данных для операций с плавающей запятой, ведущих к этим магазинам, вероятно, не так, как кажется.

Намного проще, попробуйте сделать генерацию кода более детерминированной, отключив всю оптимизацию компилятора (например, -O0 в GCC) и переписав выражения с плавающей запятой, чтобы не использовать временные/автоматические переменные. Затем разбейте каждую строку в GDB и сравните.

Я хочу, чтобы я мог точно сказать, почему этот наименее значимый бит мантиссы перевернулся, но, по правде говоря, процессор даже не "знает", почему что-то несло что-то, а что-то еще не произошло, например, порядок оценки без полной инструкции и трассировки данных как вашего кода, так и самого GDB.

Ответ 3

Его не GDB и процессор, это память и процессор. Процессор x64 хранит больше бит точности, чем фактически сохраняет память (80ish против 64 бит). Пока он остается в процессоре и регистре, он сохраняет 80 бит бит точности, но когда он будет отправлен в память, будет определяться, когда и, следовательно, как он округляется. Если GDB отправляет все результаты прерывистых вычислений из ЦП (я не знаю, если это так, или где-нибудь близко), он будет делать округление на каждом шаге, что приведет к несколько иным результатам.