Параллелизм: несколько разные результаты с плавающей запятой?

Я пытаюсь отлаживать свою библиотеку параллелизма для языка программирования D. Недавно был отправлен отчет об ошибке, в котором указано, что младшие биты некоторых плавающих точечные операции, выполняемые с помощью задач, недетерминированы между запусками. (Если вы читали отчет, обратите внимание, что parallel reduce работает под капотом, создавая задачи детерминированным образом.)

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

Редактировать: я занимаюсь отладкой printf здесь, и кажется, что результаты для отдельных задач иногда различаются при выполнении.

Редактировать № 2: Следующий код воспроизводит эту проблему гораздо проще. Он суммирует элементы массива в основном потоке, а затем запускает новый поток для выполнения той же самой функции. Проблема определенно не в моей библиотеке, потому что этот код даже не использует мою библиотеку.

import std.algorithm, core.thread, std.stdio, core.stdc.fenv;

real sumRange(const(real)[] range) {
    writeln("Rounding mode:  ", fegetround);  // 0 from both threads.
    return reduce!"a + b"(range);
}

void main() {
    immutable n = 1_000_000;
    immutable delta = 1.0 / n;

    auto terms = new real[1_000_000];
    foreach(i, ref term; terms) {
        immutable x = ( i - 0.5 ) * delta;
        term = delta / ( 1.0 + x * x ) * 1;
    }

    immutable res1 = sumRange(terms);
    writefln("%.19f", res1);

    real res2;
    auto t = new Thread( { res2 = sumRange(terms); } );
    t.start();
    t.join();
    writefln("%.19f", res2);
}

Выход:

Режим округления: 0

0.7853986633972191094

Режим округления: 0

0.7853986633972437348

Еще одно изменение

Вот вывод, когда я печатаю в шестнадцатеричном формате:

Режим округления: 0

0x1.921fc60b39f1331cp-1

Режим округления: 0

0x1.921fc60b39ff1p-1

Кроме того, это, похоже, происходит только в Windows. Когда я запускаю этот код на виртуальной машине Linux, я получаю одинаковый ответ для обоих потоков.

ОТВЕТ. Оказывается, основной причиной является то, что состояние с плавающей запятой инициализируется в основном потоке иначе, чем в других потоках в Windows в D. См. отчет об ошибке, который я только что отправил.


person dsimcha    schedule 16.04.2011    source источник
comment
Я уверен, что вы гуглили, но эта ссылка описывает несколько причин, по которым разные архитектуры ЦП могут давать разные результаты с плавающей запятой (даже несмотря на то, что они оба реализуют IEEE 754) network-theory.co.uk/docs/gccintro/gccintro_70.html   -  person Matt Bridges    schedule 16.04.2011
comment
@Matt: Извините за недоразумение. Я говорю о разных потоках на одном и том же оборудовании и даже об одном и том же физическом процессоре. Я только что убедился, что это происходит на одноядерных машинах, если вы все еще используете несколько потоков.   -  person dsimcha    schedule 16.04.2011
comment
В отчете об ошибке, на который вы ссылаетесь, упоминается ассоциативность - может ли это быть причиной вашей проблемы? (т. е. вы не добавляете одни и те же числа в одном и том же порядке каждый раз.)   -  person user541686    schedule 16.04.2011
comment
@Mehrdad: Если в моем коде нет какой-либо детали, которая ускользнула из моей памяти, порядок суммирования является детерминированным. Можно было бы ожидать несколько иных результатов, если бы количество потоков изменилось или что-то в этом роде, но не для одинаковых значений количества потоков/размеров рабочих единиц/и т. д.   -  person dsimcha    schedule 16.04.2011
comment
@dsimcha: Хм... не могли бы вы опубликовать здесь точный код?   -  person user541686    schedule 16.04.2011
comment
@Mehrdad: для этого требуется установить мой модуль и т. д. Я не уверен, что это лучшая идея. Если вы имеете в виду код соответствующих частей моего модуля, он будет слишком длинным.   -  person dsimcha    schedule 16.04.2011
comment
@dsimcha: Ну, нам нужно что-то, чтобы воспроизвести вашу ошибку с помощью кода... вы всегда можете вставить в PasteBin если код слишком длинный.   -  person user541686    schedule 16.04.2011
comment
Вы уверены, что режим округления - это то, что вы думаете? Вы пытались зарегистрировать режим округления в различных точках, чтобы быть абсолютно уверенным, что что-то еще не меняет его без вашего ведома?   -  person DK.    schedule 16.04.2011
comment
Вы пробовали печатать как Hex? Какая-то ошибка форматирования объяснила бы симптомы.   -  person BCS    schedule 16.04.2011
comment
Я получаю те же результаты (0,7853986633972191094), запуская код вашего примера. (iC2D, osx) Хм, может ли это быть как-то связано с тем, что состояние FPU не переключается между потоками или что-то в этом роде? (Понятия не имею.)   -  person 0scar    schedule 16.04.2011


Ответы (1)


Вот документ, объясняющий множество причин, по которым один и тот же код C может привести к несколько разным результатам. В вашем случае наиболее вероятной причиной является переупорядочение внутренних инструкций ЦП.

Просто неправильно ожидать, что вычисления с плавающей запятой будут детерминированными вплоть до младших битов. Это не то, для чего были созданы числа с плавающей запятой.

person Michael Borgwardt    schedule 16.04.2011
comment
Я просмотрел газету и понял основы того, что там написано. Однако в данном случае мы говорим об одном и том же двоичном коде (а не просто об одном и том же исходном коде), работающем на одном и том же оборудовании. Единственная разница в том, в каком потоке он работает. - person dsimcha; 16.04.2011
comment
После быстрого просмотра этого я не увидел, где говорится о переупорядочении инструкций на уровне ЦП. Кроме того, насколько я понимаю, если какая-либо инструкция, переупорядочивающая ЦП, привела к разнице хотя бы в один бит, то это следует считать ошибкой ЦП. -- OTOH такие же вещи на уровне компилятора (незначительные изменения в коде, приводящие к разным местам округления и т. д.) упоминались и являются реальной проблемой. (Кстати, в D/x86 real — это 80-битное число с плавающей запятой, так что математика никогда не должна усекаться.) - person BCS; 16.04.2011
comment
@dsimcha, @BCS: вы правы, я забыл об этом (здесь упоминается только переупорядочение инструкций, выполняемое компилятором). Но поскольку задействована многопоточность, может ли разница быть связана с тем, что 80-битные значения регистров усекаются до 64-битных, когда поток перемещается между ядрами? - person Michael Borgwardt; 16.04.2011
comment
Любая ОС, которая обрезает регистр FP во время переключения контекста, давно бы считалась неисправной. - person BCS; 17.04.2011