Почему этот расчет дает разные результаты в boost:: thread и std:: thread?


Когда это вычисление с плавающей запятой выполняется в boost::thread, оно дает другой результат, чем при выполнении в std::thread или в основном потоке.

void print_number()
{
    double a = 5.66;
    double b = 0.0000001;
    double c = 500.4444;
    double d = 0.13423;
    double v = std::sin(d) * std::exp(0.4 * a + b) / std::pow(c, 2.3);

    printf("%llXn%0.25fn", *reinterpret_cast<unsigned long long*>(&v), v);
}

Это, по-видимому, происходит потому, что boost::thread по умолчанию использует 53-битную внутреннюю точность для математики с плавающей точкой, в то время как основной поток использует 64-битную точность. Если состояние блока FPU сбрасывается с помощью _fpreset() после создания boost::thread, то результат будет таким же, как и в основном потоке.

Я использую Embarcadero C++ Builder 10.1 (компилятор bcc32c версии 3.3.1) и Boost 1.55.0. Моя среда-Windows 7,и я создаю для 32-разрядной целевой Windows.

Рабочий пример:

#include <tchar.h>
#include <thread>
#include <boost/thread.hpp>
#include <cstdio>
#include <cmath>
#include <cfloat>

namespace boost { void tss_cleanup_implemented() {} }

void print_number()
{
    double a = 5.66;
    double b = 0.0000001;
    double c = 500.4444;
    double d = 0.13423;
    double v = std::sin(d) * std::exp(0.4 * a + b) / std::pow(c, 2.3);

    // Edit:
    // Avoiding the undefined behaviour by a reinterpret_cast, as
    // mentioned in some answers and comments.
    unsigned long long x;
    memcpy(&x, &v, sizeof(x));

    printf("%llXn%0.25fn", x, v);
}

void print_number_2()
{
    // Reset FPU precision to default
    _fpreset();
    print_number();
}

int _tmain(int argc, _TCHAR* argv[])
{
    print_number();

    std::thread t1(&print_number);
    t1.join();

    boost::thread t2(&print_number);
    t2.join();

    boost::thread t3(&print_number_2);
    t3.join();

    getchar();
    return 0;
}

Вывод:

3EAABB3194A6E99A
0.0000007966525939409087744
3EAABB3194A6E99A
0.0000007966525939409087744
3EAABB3194A6E999
0.0000007966525939409087488
3EAABB3194A6E99A
0.0000007966525939409087744

Вопрос:

  • Почему это происходит? Разве новый поток не должен наследовать среду с плавающей запятой от родительского потока?
  • это ошибка в компиляторе или в Boost, или мои ожидания неверны?
4 14

4 ответа:

Разница, по-видимому, заключается в том, что реализация std::thread делает _fpreset(), а boost::thread, очевидно, нет. ]}

namespace boost { void tss_cleanup_implemented() { } }

To (отформатировано немного для ясности):

namespace boost 
{ 
    void tss_cleanup_implemented() 
    { 
        _fpreset(); 
    }
}
Вы увидите, что все значения теперь совершенно одинаковы (3EAABB3194A6E99A). Это говорит мне,что Boost не делает _fpreset(). Этот вызов необходим, потому что некоторые вызовы Windows API портят стандартные параметры FPU, используемые C++Builder (32 бит), и не возвращают их к тому, что они были (это проблема, с которой вы можете столкнуться и в Delphi).

И std::thread, и boost:thread используют вызовы Win32 API для обработки потоков.

Что-то подсказывает мне, что вы уже ожидали этого, поэтому тест с print_number_2(), который делает _fpreset().

Это: *reinterpret_cast<unsigned long long*>(&v) является неопределенным поведением, поскольку v не является unsigned_long_long. Если вы хотите скопировать двоичное представление double в интегральный тип, используйте memcpy(). Обратите внимание, что даже с memcpy(), это реализация, определяющая, как будет выглядеть двоичное представление, но вы гарантированно можете "загрузить обратно то, что вы сохранили". Ничего больше АФАИК.

Это не разница между 64 и 53-битными вычислениями точности FPU, это разница вокруглении . Единственное различие между этими двумя результатами заключается в наименее значимой части ответа. Похоже, что начальный код потока boost неправильно инициализирует флаги FPU, а режим округления по умолчанию-down или chop, а не ближайший.

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

Для этого вам нужен лучший компилятор.


Это, по-видимому, происходит потому, что boost::thread по умолчанию использует 53-битную внутреннюю точность для математики с плавающей запятой, в то время как основной поток использует 64-битную точность. Если состояние блока FPU сбрасывается с помощью функции _fpreset () после создания потока boost::, то результат будет таким же, как и в основном потоке.

Это безумие. Если компилятор использует другой модуль FP (например, x87 vs SSE) для разных областей кода, вы должны сжечь этот компилятор с самым большим огнем, который вы можете найти.

Запуск этого кода под g++-6.1 и clang++-3.8 на Linux Mint 17.3 дает идентичные результаты для каждого типа потока.

#include <thread>
#include <boost/thread.hpp>
#include <cstdio>
#include <cmath>

void print_number() {
    double a = 5.66;
    double b = 0.0000001;
    double c = 500.4444;
    double d = 0.13423;
    double v = std::sin(d) * std::exp(0.4 * a + b) / std::pow(c, 2.3);

    printf("%llX\n%0.25f\n", *reinterpret_cast<unsigned long long*>(&v), v);
}

int main() {
    print_number();

    std::thread t1(&print_number);
    t1.join();

    boost::thread t2(&print_number);
    t2.join();
}

CXX-std=c++14-O3-c test test.c-pthread-lboost_thread-lboost_system

3EAABB3194A6E999
0.0000007966525939409086685

3EAABB3194A6E999
0.0000007966525939409086685

3EAABB3194A6E999
0.0000007966525939409086685


Как отметил @lorro в своем ответе, вы нарушаете правила псевдонимирования в reinterpret_cast.