Ответ 1
По-видимому, 64-битная реализация OpenMP в MSVC несовместима с кодом, скомпилированным без оптимизаций.
Чтобы отладить вашу проблему, я изменил свой код, чтобы сохранить номер итерации в глобальной переменной threadprivate
непосредственно перед вызовом this->eval()
, а затем добавил проверку в начале Implementation::eval()
, чтобы узнать, сохраненный номер итерации отличается от elem.i_
:
static int _iter;
#pragma omp threadprivate(_iter)
...
#pragma omp parallel for default(none) shared(N, dim, src, res)
for (int i = 0; i < N; ++i) {
assert(i < N);
double *r = res + i * dim;
Element elem(i, &src);
assert(elem.i() == i); // Point (1)
_iter = i; // Save the iteration number
this->eval(dim, elem, r);
}
}
...
...
static void eval (int dim, Element elem, double *r)
{
// Check for difference
if (elem.i() != _iter)
printf("[%d] _iter=%x != %x\n", omp_get_thread_num(), _iter, elem.i());
assert(elem.i() < elem.size()); // This is where the program fails Point (4)
for (int d = 0; d != dim; ++d)
r[d] = elem.src();
}
...
Похоже, что случайное значение elem.i_
становится плохой смесью значений, переданных в разных потоках, в void eval_dispatch(int dim, Element elem, double *res, void (*) (int, Element, double *))
. Это происходит в каждом прогоне, но вы видите его только после того, как значение elem.i_
становится достаточно большим, чтобы вызвать это утверждение. Иногда бывает, что смешанное значение не превышает размер контейнера, а затем код завершает выполнение без утверждения. Также, что вы видите во время сеанса отладки после утверждения, является невозможностью отладчика VS правильно справиться с многопоточным кодом:)
Это происходит только в неоптимизированном 64-битном режиме. Это не происходит в 32-битном коде (как отладка, так и выпуск). Это также не происходит в 64-битном коде выпуска, если оптимизация не отключена. Это также не происходит, если вы вызываете вызов this->eval()
в критическом разделе:
#pragma omp parallel for default(none) shared(N, dim, src, res)
for (int i = 0; i < N; ++i) {
...
#pragma omp critical
this->eval(dim, elem, r);
}
}
но это отменит преимущества OpenMP. Это показывает, что что-то дальше по цепочке вызовов выполняется небезопасным способом. Я изучил код сборки, но не смог найти точную причину. Я действительно озадачен, так как MSVC реализует неявный конструктор копирования класса Element
, используя простую побитовую копию (она даже встроенная), и все операции выполняются в стеке.
Это напоминает мне о том, что компилятор Sun (теперь Oracle) настаивает на том, что он должен повысить уровень оптимизации, если вы поддерживаете поддержку OpenMP. К сожалению, документация опции /openmp
в MSDN ничего не говорит о возможном вмешательстве, которое может исходить из "неправильного" уровня оптимизации. Это также может быть ошибкой. Я должен проверить с другой версией VS, если я могу получить доступ к ней.
Изменить: Я вырыл глубже, как было обещано, и запустил код в Intel Parallel Inspector 2011. Он обнаружил один шаблон расы данных, как и ожидалось. По-видимому, когда эта строка выполняется:
this->eval(dim, elem, r);
создается временная копия elem
и передается по адресу методу eval()
, как это требуется в ABI для Windows x64. И вот странная вещь: расположение этой временной копии не входит в стек funclet, который реализует параллельную область (компилятор MSVC вызывает его Evaluator$omp$1<Implementation>::operator()
кстати), как и следовало ожидать, но скорее его адрес берется как первый аргумент funclet. Поскольку этот аргумент один и тот же во всех потоках, это означает, что временная копия, которая далее передается в this->eval()
, фактически разделяется между всеми потоками, что смешно, но по-прежнему верно, как легко заметить:
...
void eval (int dim, Element elem, double *res)
{
printf("[%d] In Base::eval() &elem = %p\n", omp_get_thread_num(), &elem);
// Dispatch the call from Evaluation<Derived>
eval_dispatch(dim, elem, res, &Derived::eval); // Point (2)
}
...
...
#pragma omp parallel for default(none) shared(N, dim, src, res)
for (int i = 0; i < N; ++i) {
...
Element elem(i, &src);
...
printf("[%d] In parallel region &elem = %p\n", omp_get_thread_num(), &elem);
this->eval(dim, elem, r);
}
}
...
Запуск этого кода создает такой же результат:
[0] Parallel region &elem = 000000000030F348 (a)
[0] Base::eval() &elem = 000000000030F630
[0] Parallel region &elem = 000000000030F348 (a)
[0] Base::eval() &elem = 000000000030F630
[1] Parallel region &elem = 000000000292F9B8 (b)
[1] Base::eval() &elem = 000000000030F630 <---- !!
[1] Parallel region &elem = 000000000292F9B8 (b)
[1] Base::eval() &elem = 000000000030F630 <---- !!
Как и ожидалось, elem
имеет разные адреса в каждом потоке, выполняющем параллельную область (точки (a)
и (b)
). Но обратите внимание, что временная копия, которая передается в Base::eval()
, имеет один и тот же адрес в каждом потоке. Я считаю, что это ошибка компилятора, которая заставляет неявный конструктор копирования Element
использовать общую переменную. Это можно легко проверить, просмотрев адрес, переданный в Base::eval()
- он находится где-то между адресом N
и адресом src
, то есть в блоке с разделяемыми переменными. Дальнейшая проверка источника сборки показывает, что действительно адрес временного места передается как аргумент функции _vcomp_fork()
из vcomp100.dll
, которая реализует часть fork модели OpenMP fork/join.
Поскольку в принципе нет параметров компилятора, которые могут влиять на это поведение, кроме возможности оптимизаций, которые приводят к тому, что все Base::eval()
, Base::eval_dispatch()
и Implementation::eval()
все являются встроенными и, следовательно, временные копии elem
единственные обходные пути, которые я нашел:
1) Сделайте аргумент Element elem
в Base::eval()
ссылкой:
void eval (int dim, Element& elem, double *res)
{
eval_dispatch(dim, elem, res, &Derived::eval); // Point (2)
}
Это гарантирует, что локальная копия elem
в стеке funclet, реализующего параллельную область в Evaluator<Implementation>::operator()
, передается, а не временная копия общего доступа. Это дополнительно передается по значению в качестве другой временной копии в Base::eval_dispatch()
, но сохраняет правильное значение, поскольку эта новая временная копия находится в стеке Base::eval()
, а не в блоке с разделяемыми переменными.
2) Предоставьте явный конструктор копирования Element
:
Element (const Element& e) : i_(e.i_), src_(e.src_) {}
Я бы порекомендовал вам пойти с явным конструктором копирования, поскольку он не требует дальнейших изменений в исходном коде.
По-видимому, это поведение также присутствует в MSVS 2008. Мне нужно будет проверить, присутствует ли он в MSVS 2012 и, возможно, файл с сообщением об ошибке с MS.
Эта ошибка не отображается в 32-битном коде, так как все значение, переданное объектом value, помещается в стек вызовов, а не только указатель на него.