Разные результаты между Debug и Release
У меня проблема в том, что мой код возвращает разные результаты при сравнении отладки и выпуска. Я проверил, что оба режима используют / fp: точный, так что проблем не должно быть. Основная проблема, с которой я столкнулся, заключается в том, что полный анализ изображений (это проект по пониманию изображений) является полностью детерминированным, в этом нет абсолютно ничего случайного.
Еще одна проблема, связанная с этим, заключается в том, что моя сборка выпуска фактически всегда возвращает один и тот же результат (23,014 для образа), тогда как отладка возвращает случайное значение между 22 и 23, чего просто не должно быть. Я уже проверил, может ли это быть связано с потоками, но единственная часть алгоритма, которая является многопоточной, возвращает точно такой же результат как для отладки, так и для выпуска.
Что еще может происходить здесь?
Обновление 1: код, который я теперь нашел ответственным за это поведение:
float PatternMatcher::GetSADFloatRel(float* sample, float* compared, int sampleX, int compX, int offX)
{
if (sampleX != compX)
{
return 50000.0f;
}
float result = 0;
float* pTemp1 = sample;
float* pTemp2 = compared + offX;
float w1 = 0.0f;
float w2 = 0.0f;
float w3 = 0.0f;
for(int j = 0; j < sampleX; j ++)
{
w1 += pTemp1[j] * pTemp1[j];
w2 += pTemp1[j] * pTemp2[j];
w3 += pTemp2[j] * pTemp2[j];
}
float a = w2 / w3;
result = w3 * a * a - 2 * w2 * a + w1;
return result / sampleX;
}
Обновление 2: это не воспроизводится с 32-битным кодом. Хотя код отладки и выпуска всегда будет приводить к одному и тому же значению для 32-разрядной версии, он все равно будет отличаться от 64-разрядной версии выпуска, а отладка для 64-разрядной версии по-прежнему возвращает некоторые совершенно случайные значения.
Обновление 3: Хорошо, я обнаружил, что это, безусловно, вызвано OpenMP. Когда я его отключаю, все работает нормально. (и Debug, и Release используют один и тот же код, и оба активировали OpenMP).
Ниже приведен код, который доставляет мне неприятности:
#pragma omp parallel for shared(last, bestHit, cVal, rad, veneOffset)
for(int r = 0; r < 53; ++r)
{
for(int k = 0; k < 3; ++k)
{
for(int c = 0; c < 30; ++c)
{
for(int o = -1; o <= 1; ++o)
{
/*
r: 2.0f - 15.0f, in 53 steps, representing the radius of blood vessel
c: 0-29, in steps of 1, representing the absorption value (collagene)
iO: 0-2, depending on current radius. Signifies a subpixel offset (-1/3, 0, 1/3)
o: since we are not sure we hit the middle, move -1 to 1 pixels along the samples
*/
int offset = r * 3 * 61 * 30 + k * 30 * 61 + c * 61 + o + (61 - (4*w+1))/2;
if(offset < 0 || offset == fSamples.size())
{
continue;
}
last = GetSADFloatRel(adapted, &fSamples.at(offset), 4*w+1, 4*w+1, 0);
if(bestHit > last)
{
bestHit = last;
rad = (r+8)*0.25f;
cVal = c * 2;
veneOffset =(-0.5f + (1.0f / 3.0f) * k + (1.0f / 3.0f) / 2.0f);
if(fabs(veneOffset) < 0.001)
veneOffset = 0.0f;
}
last = GetSADFloatRel(input, &fSamples.at(offset), w * 4 + 1, w * 4 + 1, 0);
if(bestHit > last)
{
bestHit = last;
rad = (r+8)*0.25f;
cVal = c * 2;
veneOffset = (-0.5f + (1.0f / 3.0f) * k + (1.0f / 3.0f) / 2.0f);
if(fabs(veneOffset) < 0.001)
veneOffset = 0.0f;
}
}
}
}
}
Примечание: при активированном режиме Release и OpenMP я получаю тот же результат, что и при деактивации OpenMP. Режим отладки и активация OpenMP дает другой результат, деактивированный OpenMP - тот же результат, что и в Release.
5 ответов
Чтобы уточнить мой комментарий, это код, который, скорее всего, является корнем вашей проблемы:
#pragma omp parallel for shared(last, bestHit, cVal, rad, veneOffset)
{
...
last = GetSADFloatRel(adapted, &fSamples.at(offset), 4*w+1, 4*w+1, 0);
if(bestHit > last)
{
last
присваивается только перед повторным чтением, поэтому это хороший кандидат на lastprivate
переменная, если вам действительно нужно значение из последней итерации за пределами параллельной области. В противном случае просто сделайте это private
,
Доступ к bestHit
, cVal
, rad
, а также veneOffset
должны быть синхронизированы по критической области:
#pragma omp critical
if (bestHit > last)
{
bestHit = last;
rad = (r+8)*0.25f;
cVal = c * 2;
veneOffset =(-0.5f + (1.0f / 3.0f) * k + (1.0f / 3.0f) / 2.0f);
if(fabs(veneOffset) < 0.001)
veneOffset = 0.0f;
}
Обратите внимание, что по умолчанию все переменные, кроме счетчиков parallel for
петли и те, которые определены внутри параллельной области, являются общими, то есть shared
пункт в вашем случае ничего не делает, если вы также не применяете default(none)
пункт.
Еще одна вещь, о которой вы должны знать, это то, что в 32-битном режиме Visual Studio использует математику x87 FPU, тогда как в 64-битном режиме она по умолчанию использует математику SSE. x87 FPU выполняет промежуточные вычисления с использованием 80-битной точности с плавающей запятой (даже для вычислений, включающих float
только), в то время как блок SSE поддерживает только стандарт IEEE одинарной и двойной точности. Введение OpenMP или любого другого метода распараллеливания в 32-битный код x87 FPU означает, что в определенных точках промежуточные значения должны быть преобразованы обратно с одинарной точностью float
и если делать это достаточно много раз, можно наблюдать небольшую или значительную разницу (в зависимости от числовой стабильности алгоритма) между результатами последовательного кода и параллельного кода.
Исходя из вашего кода, я бы предположил, что следующий модифицированный код даст вам хорошую параллельную производительность, потому что нет синхронизации на каждой итерации:
#pragma omp parallel private(last)
{
int rBest = 0, kBest = 0, cBest = 0;
float myBestHit = bestHit;
#pragma omp for
for(int r = 0; r < 53; ++r)
{
for(int k = 0; k < 3; ++k)
{
for(int c = 0; c < 30; ++c)
{
for(int o = -1; o <= 1; ++o)
{
/*
r: 2.0f - 15.0f, in 53 steps, representing the radius of blood vessel
c: 0-29, in steps of 1, representing the absorption value (collagene)
iO: 0-2, depending on current radius. Signifies a subpixel offset (-1/3, 0, 1/3)
o: since we are not sure we hit the middle, move -1 to 1 pixels along the samples
*/
int offset = r * 3 * 61 * 30 + k * 30 * 61 + c * 61 + o + (61 - (4*w+1))/2;
if(offset < 0 || offset == fSamples.size())
{
continue;
}
last = GetSADFloatRel(adapted, &fSamples.at(offset), 4*w+1, 4*w+1, 0);
if(myBestHit > last)
{
myBestHit = last;
rBest = r;
cBest = c;
kBest = k;
}
last = GetSADFloatRel(input, &fSamples.at(offset), w * 4 + 1, w * 4 + 1, 0);
if(myBestHit > last)
{
myBestHit = last;
rBest = r;
cBest = c;
kBest = k;
}
}
}
}
}
#pragma omp critical
if (bestHit > myBestHit)
{
bestHit = myBestHit;
rad = (rBest+8)*0.25f;
cVal = cBest * 2;
veneOffset =(-0.5f + (1.0f / 3.0f) * kBest + (1.0f / 3.0f) / 2.0f);
if(fabs(veneOffset) < 0.001)
veneOffset = 0.0f;
}
}
Он хранит только значения параметров, которые дают лучший результат в каждом потоке, а затем в конце параллельной области, которую он вычисляет. rad
, cVal
а также veneOffset
на основе лучших ценностей. Теперь есть только одна критическая область, и она находится в конце кода. Вы также можете обойти это, но вам придется ввести дополнительные массивы.
Как минимум две возможности:
- Включение оптимизации может привести к операциям переупорядочения компилятора. Это может привести к небольшим различиям в вычислениях с плавающей запятой по сравнению с порядком, выполняемым в режиме отладки, где переупорядочение операций не происходит. Это может учитывать числовые различия между отладкой и выпуском, но не учитывает числовые различия от одного прогона к следующему в режиме отладки.
- В вашем коде есть ошибка, связанная с памятью, такая как чтение / запись за пределы массива, использование неинициализированной переменной, использование нераспределенного указателя и т. Д. Попробуйте запустить ее через средство проверки памяти, такое как превосходный Valgrind, чтобы выявить такие проблемы. Ошибки, связанные с памятью, могут объяснять недетерминированное поведение.
Если вы работаете в Windows, то Valgrind недоступен (жаль), но вы можете посмотреть здесь список альтернатив.
Одна вещь, чтобы проверить дважды, что все переменные инициализированы. Много раз неоптимизированный код (режим отладки) будет инициализировать память.
Почти любое неопределенное поведение может объяснять это: неинициализированные переменные, мошеннические указатели, множественные модификации одного и того же объекта без промежуточной точки последовательности и т. Д. И т. Д. Тот факт, что результаты иногда являются невоспроизводимыми, в некоторой степени свидетельствует о неинициализированной переменной, но это также может возникнуть из-за проблем с указателями или ошибок границ.
Помните, что оптимизация может изменить результаты, особенно на Intel. Оптимизация может изменить то, какие промежуточные значения выливаются в память, и, если вы не использовали аккуратно скобки, даже порядок вычисления в выражении. (И, как мы все знаем, в машине с плавающей точкой, (a +
b) + c) != a + (b + c)
.) Тем не менее, результаты должны быть детерминированными: вы получите разные результаты в зависимости от степени оптимизации, но для любого набора флагов оптимизации вы должны получить те же результаты.
Я бы сказал, инициализация переменной в отладке, а не в выпуске. Но ваши результаты не подтвердят это (надежный результат в выпуске).
Ваш код полагается на какие-то определенные смещения или размеры? Отладочная сборка поместит защитные байты вокруг некоторых выделений.
Может ли это быть связано с плавающей запятой?
Стек отладочной плавающей запятой отличается от выпуска, созданного для большей эффективности.
Смотрите здесь: http://thetweaker.wordpress.com/2009/08/28/debugrelease-numerical-differences/