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

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

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

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


person Brian Cole    schedule 15.02.2011    source источник
comment
Не могли бы вы добавить ссылку на предложение о переключении контекста? Хотя я могу представить процессор, перемещающий данные аккумулятора и отбрасывающий биты, этот механизм не кажется мне хорошим объяснением, и некоторые подробности могут быть интересны.   -  person Coffee on Mars    schedule 15.02.2011
comment
Возможно, использование других флагов оптимизации компилятора может решить эту проблему.   -  person tkerwin    schedule 15.02.2011
comment
@Coffee on Mars: Это должно было быть моим предложением, поэтому я думаю, что могу объяснить :) Проблема в том, что FPU может использовать большее количество битов в своих регистрах, в некоторых последних процессорах до 80 бит для удвоения. . Теперь в однопоточной среде FPU сможет выполнять все операции с такой точностью, и вы получите один результат. Если вы добавите в смесь другие потоки/процессы, когда ОС выполняет переключение контекста, она должна сохранить значение 80-битного регистра в 64-битном двойном, теряя точность.   -  person David Rodríguez - dribeas    schedule 15.02.2011
comment
Быть не ответом, а «догадкой». Для работающего программного обеспечения было бы мудрее, если бы оно выполняло сравнение в целочисленной форме числа с плавающей запятой, например, преобразовывая число с плавающей запятой в форму знака-показатель-мантисса беззнакового длинного. Это также подразумевает сохранение значений эксперимента в этой форме, поэтому у вас не будет проблем, когда числа будут слишком близко друг к другу.   -  person Giuliano    schedule 15.02.2011
comment
Чтобы проверить вашу теорию избыточных битов, я могу предложить объявить и установить новую переменную перед сравнением. Это снизит производительность и никоим образом не может быть решением, но было бы интересным способом отвергнуть вашу гипотезу.   -  person Matthew    schedule 15.02.2011
comment
Кроме того, ваша программа многопоточная или распределенная? Единственные случаи, когда я столкнулся с такими несоответствиями, были с (тонкими) условиями гонки. Также MPI может быть возможным виновником, если вы его используете.   -  person Alexandre C.    schedule 16.02.2011
comment
@Matthew PK, другой подход, который фактически заставляет компилятор записывать в строку кэша (оптимизатор может удалить дополнительную переменную), вы можете объявить переменные volatile. Это немного снизит производительность, так как принудительно будет проходить кеш L1 (и если есть ложное совместное использование, это может закончиться переходом к кешу L2-L3).   -  person David Rodríguez - dribeas    schedule 16.02.2011
comment
Извините за пренебрежение важной информацией, такой как платформа и ОС, так как я должен был закончить отправку в спешке. Расчеты ведутся на платформе x86_64 и 64-битной Linux (Scientific linux). Вот некоторые файлы /proc/cpuinfo и /proc/version:   -  person Brian Cole    schedule 16.02.2011
comment
proc/version: Linux version 2.6.18-238.1.1.el5 gcc version 4.1.2 20080704 (Red Hat 4.1.2-50) Обратите внимание, что Scientific Linux является вариантом RedHat   -  person Brian Cole    schedule 16.02.2011
comment
@David Rodriguez: это была ситуация, о которой я думал, но проблема, которую, как мне казалось, я увидел в ней, заключалась в том, что можно было ожидать, что процессор сохранит точное состояние регистров, а не сделает логическое преобразование в его наиболее значимое логическое ближайшее государство. Бо Перссон в комментарии ниже цитирует инструкции FSAVE/FRSTOR, которые, как я ожидаю, будут использоваться в ситуации переключения контекста.   -  person Coffee on Mars    schedule 16.02.2011


Ответы (7)


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

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

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

person Jerry Coffin    schedule 15.02.2011
comment
Могу поспорить, что ОС использует инструкции FSAVE/FRSTOR, которые фактически сбрасывают и восстанавливают все биты состояний x87. Сюда входят все 80 бит внутренних регистров. Предполагая, что x86 с 32-битной ОС, которую Брайан забыл сообщить нам, использует ли он. :-) - person Bo Persson; 15.02.2011
comment
@Bo Persson: регистры FP имеют 80 видимых битов, но большинство из них также имеют по крайней мере один или два защитных бита, чтобы дать правильно округленный младший бит в видимых 80. - person Jerry Coffin; 16.02.2011
comment
Все такие преобразования и сохранение/восстановление состояния являются детерминированными. Сохраненное состояние FP является идеальной копией. - person zvrba; 16.02.2011

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

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

person Martin Beckett    schedule 15.02.2011
comment
И как это приведет к недетерминированным результатам? - person zvrba; 16.02.2011
comment
Переключение контекста, как описано Джерри - person Martin Beckett; 16.02.2011

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

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

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

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

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

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

person zvrba    schedule 15.02.2011
comment
Программа не является многопоточной, хотя выполняется в многопроцессном контексте. И он прошел через Valgrind (включая Memcheck) без проблем. В отчаянии из-за того, что не удалось определить источник невоспроизводимости, мы прибегли к несложной отладке — сбрасывая сравниваемое значение с аналитическим разрезом (0,3) в cout. Два отдельных выполнения печатают точность от 0,3 до 15 значащих цифр ( ... ‹‹ std::setw(15) ‹‹ dR ), но следующая строка, которая выполняет сравнение dR с 0,3, дает другой результат. - person Brian Cole; 16.02.2011
comment
почему вы не записываете шестнадцатеричные дампы значений переменных? log2 (10 ^ 15) составляет ~ 49,82, а в long double больше битов мантиссы. - person Shelwien; 16.02.2011
comment
Хорошо, хотя valgrind не всегда обнаруживает все проблемы. Я предлагаю вам выполнять отладку на уровне машинного кода. Установите точку останова на операторе печати, выполняйте инструкцию за инструкцией и проверяйте состояние всех регистров после каждой инструкции. Не забудьте также посмотреть регистры XMM. Можете ли вы вставить сюда рассматриваемый код (что связано с оператором печати и самим сравнением)? - person zvrba; 16.02.2011
comment
Я согласен с этим анализом. Я искренне сомневаюсь в переключении контекста, если, конечно, я не ошибаюсь. Я не верю, что состояния регистров помещаются в стек неправильно. - person Alexandre C.; 16.02.2011
comment
В 2016 году, если это все еще проблема, вы могли бы перекомпилировать вместо этого -fsanitize=undefined. Я думаю, это должно поймать больше арифметических ошибок. - person Jean-Michaël Celerier; 08.10.2016
comment
Это также может быть какая-то часть кода или сторонняя библиотека, которую он загружает/связывает с плохим поведением и оставляет режим округления с плавающей запятой (или другую конфигурацию с плавающей запятой во время выполнения) в состоянии, отличном от значения по умолчанию. Конечно, эта библиотека также должна делать что-то недетерминированное (например, случайные числа, ввод-вывод или многопоточность), но это возможно. - person ndkrempel; 23.06.2021

Я сделал это:

#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 [?x@@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 сразу, и в любом случае ОС, которая не будет сохранять полную точность, будет полностью сломана .

Таким образом, либо это какая-то уникальная ОС/процессор/компилятор, либо после изменения чего-либо в программе и ее перекомпиляции получаются разные результаты сравнения, либо это ошибка в программе, например. переполнение буфера.

person Shelwien    schedule 15.02.2011

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

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

person Puppy    schedule 15.02.2011
comment
Хорошая попытка, но все такие преобразования детерминированы. Состояние FP сохраняется с помощью специальных инструкций, и такой механизм сохранения/восстановления не теряет информацию. - person zvrba; 16.02.2011
comment
@zvrba: детерминированный не означает без потерь. Инструкция, которая является детерминированной, не ведет себя детерминированной, если вызывается недетерминированной. - person Puppy; 16.02.2011
comment
Хм? Все инструкции FP дают точно такие же (даже побитовые!) результаты при одних и тех же входных данных, независимо от того, при каких обстоятельствах они вызываются. (За возможным исключением одного из аргументов, являющегося бесконечностью NaN.) - person zvrba; 16.02.2011

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

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

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

person Coffee on Mars    schedule 15.02.2011
comment
Эти инструкции обычно не используются программой пользовательского режима, и если бы в ядре Linux была такая ошибка (т. е. использование неправильных инструкций для сохранения/восстановления состояния FP), она была бы обнаружена давно. - person zvrba; 16.02.2011
comment
Да, я верю, что ты прав. С другой стороны, хорошее переключение задач не должно оставлять после себя биты в регистрах, и такая возможность обсуждалась в комментариях к вопросу. Я оставлю ответ здесь, как ссылку на возможный запрос в сторону сборки проблемы. - person Coffee on Mars; 16.02.2011
comment
Никто здесь еще не доказал, что переключение задач оставляет биты в регистрах. Аппаратные инструкции, предназначенные для переключения состояний FP, уж точно не оставляют мусора. - person zvrba; 16.02.2011

Вы наверняка столкнулись с ошибкой GCC № 323, которая, как и другие моменты, out происходит из-за избыточной точности FPU.

Решения:

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

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

person Jean-Michaël Celerier    schedule 08.10.2016
comment
Сама по себе эта ошибка не объясняет недетерминированность при разных запусках одного и того же двоичного файла на одной и той же машине с одними и теми же входными данными. - person ndkrempel; 23.06.2021