Невоспроизводимость сравнения с плавающей точкой

Я и мой доктор философии. студент столкнулся с проблемой в контексте анализа физических данных, что я мог бы использовать некоторое понимание. У нас есть код, который анализирует данные из одного из экспериментов LHC, которые дают неприводимые результаты. В частности, результаты расчетов, полученных из одного и того же двоичного, выполняемого на той же машине, могут различаться между последовательными исполнениями. Мы знаем много разных источников невоспроизводимости, но исключили обычных подозреваемых.

Мы проследили эту проблему до невозможности (с двойной точностью) операций сравнения с плавающей запятой при сравнении двух чисел, которые номинально имеют одинаковое значение. Это может случиться из-за предыдущих шагов анализа. Например, мы просто нашли пример, который проверяет, меньше ли число 0,3 (обратите внимание, что мы НИКОГДА не проверяем равенство между плавающими значениями). Оказывается, из-за геометрии детектора время от времени можно было получить результат, который был бы точно равен 0,3 (или его ближайшему представлению с двойной точностью).

Мы хорошо осведомлены о подводных камнях при сравнении чисел с плавающей запятой, а также о возможности избыточной точности в FPU влиять на результаты сравнения. Вопрос, на который я хотел бы ответить, - "почему результаты невоспроизводимы?" Это связано с тем, что загрузка регистра FPU или другие инструкции FPU не очищают лишние биты, и, таким образом, оставшиеся бит из предыдущих вычислений влияют на результаты? (это кажется маловероятным). Я видел предложение на другом форуме, в котором контекст переключается между процессами или потоками, также может вызвать изменение результатов сравнения с плавающей запятой из-за того, что содержимое FPU хранится в стеке и, следовательно, усечено. Любые комментарии к этим = или другим возможным объяснениям будут оценены.

Ответ 1

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

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

Чтобы быть ясным: я сомневаюсь, что "оставшиеся" бит были бы виновником. Скорее, это будет потеря дополнительных битов, вызывающих округление в несколько разных точках вычисления.

Ответ 2

Какая платформа?

Большинство FPU могут внутренне хранить больше точности, чем двойное представление ieee, чтобы избежать ошибки округления в промежуточных результатах. Часто бывает, что компилятор переключается на скорость/точность торговли - см. http://msdn.microsoft.com/en-us/library/e7s85ffb(VS.80).aspx

Ответ 3

Я сделал это:

#include <stdio.h>
#include <stdlib.h>

typedef long double ldbl;

ldbl x[1<<20];

void hexdump( void* p, int N ) {
  for( int i=0; i<N; i++ ) printf( "%02X", ((unsigned char*)p)[i] );
}

int main( int argc, char** argv ) {

  printf( "sizeof(long double)=%i\n", sizeof(ldbl) );

  if( argc<2 ) return 1;

  int i;
  ldbl a = ldbl(1)/atoi(argv[1]);

  for( i=0; i<sizeof(x)/sizeof(x[0]); i++ ) x[i]=a;

  while(1) {
    for( i=0; i<sizeof(x)/sizeof(x[0]); i++ ) if( x[i]!=a ) {
      hexdump( &a, sizeof(a) );
      printf( " " );
      hexdump( &x[i], sizeof(x[i]) );
      printf( "\n" );
    }
  }

}

скомпилированный с помощью IntelC/Qlong_double, чтобы он произвел это:

;;;     for( i=0; i<sizeof(x)/sizeof(x[0]); i++ ) if( x[i]!=a ) {

        xor       ebx, ebx                                      ;25.10
                                ; LOE ebx f1
.B1.9:                          ; Preds .B1.19 .B1.8
        mov       esi, ebx                                      ;25.47
        shl       esi, 4                                        ;25.47
        fld       TBYTE PTR [[email protected]@3PA_TA+esi]                    ;25.51
        fucomp                                                  ;25.57
        fnstsw    ax                                            ;25.57
        sahf                                                    ;25.57
        jp        .B1.10        ; Prob 0%                       ;25.57
        je        .B1.19        ; Prob 79%                      ;25.57
[...]
.B1.19:                         ; Preds .B1.18 .B1.9
        inc       ebx                                           ;25.41
        cmp       ebx, 1048576                                  ;25.17
        jb        .B1.9         ; Prob 82%                      ;25.17

и начал 10 экземпляров с разными "семенами". Как вы можете видеть, оно сравнивает 10-байтовый длинный удваивается из памяти с одним в стеке FPU, поэтому в случае, когда ОС не сохраняет полную точность, мы обязательно увидим ошибку. И хорошо, они все еще работают, не обнаруживая ничего... что на самом деле что x86 имеет команды для сохранения/восстановления всего состояния FPU сразу, и в любом случае ОС, которая не сохранит полную точность, будет полностью нарушена.

Итак, либо его уникальный OS/cpu/компилятор, либо разные результаты сравнения создаются после изменения чего-либо в программе и перекомпиляции или ее ошибка в программе, например. переполнение буфера.

Ответ 4

Является ли программа многопоточной?

Если да, я бы заподозрил состояние гонки.

Если нет, выполнение программы детерминировано. Наиболее вероятным результатом для получения разных результатов при одинаковых входных данных является поведение undefined, т.е. Ошибка в вашей программе. Чтение неинициализированной переменной, устаревший указатель, переписывание младших бит некоторого номера FP в стеке и т.д. Возможности бесконечны. Если вы запускаете это в linux, попробуйте запустить его под valgrind и посмотрите, обнаруживает ли он некоторые ошибки.

Кстати, как вы сузили проблему до сравнения FP?

(Длинный снимок: сбои в оборудовании? Например, ошибка в чипе RAM может привести к тому, что данные будут считываться по-разному в разное время. Хотя это, скорее всего, приведет к сбою ОС довольно быстро.)

Любое другое объяснение неправдоподобно - ошибки в ОС или HW не исчезли бы долгое время.

Ответ 5

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

Конечно, это зависит от платформы, но ваше описание звучит так, как будто вы работаете на современном рабочем столе или сервере (так что x86).

Ответ 6

Я просто объединю некоторые комментарии Дэвида Родригеса и Бо Перссона и сделаю все возможное.

Может ли быть переключением задачи при использовании инструкций SSE3? Основываясь на этой статье Intel по использованию инструкций SSE3, команды для сохранения статуса регистра FSAVE и FRESTOR были заменены FXSAVE и FXRESTOR, которые должны обрабатывать полная длина аккумулятора.

На машине x64 я полагаю, что "некорректная" инструкция может содержаться в некоторой внешней компилируемой библиотеке.

Ответ 7

Вы наверняка нажмете GCC Bug n ° 323, что, как другие указывает, связано с избыточной точностью FPU.

Решения:

  • Использование SSE (или AVX, это 2016...) для выполнения ваших вычислений
  • Использование компилятора -ffloat-store. Из документов GCC.

Не хранить переменные с плавающей запятой в регистрах и запрещать другие параметры, которые могут изменить, будет ли значение с плавающей запятой выбрано из регистра или памяти.
 Эта опция предотвращает нежелательную избыточную точность на машинах, таких как 68000, где предполагается, что плавающие регистры (68881) имеют более высокую точность, чем двойной. Аналогично для архитектуры x86. Для большинства программ избыточная точность работает только хорошо, но несколько программ полагаются на точное определение плавающей точки IEEE. Используйте -ffloat-store для таких программ, после их модификации, чтобы хранить все соответствующие промежуточные вычисления в переменных.