我遇到了一个问题,即我的代码在比较调试与发布时返回不同的结果。我检查了两种模式是否都使用/fp:precise,所以这应该不是问题。我遇到的主要问题是完整的图像分析(它是一个图像理解项目)是完全确定性的,其中绝对没有随机性。
这样做的另一个问题是,我的发布版本实际上总是返回相同的结果(图像为 23.014),而 debug 返回 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引起的。当我禁用它时,它工作正常。(调试和发布都使用相同的代码,并且都激活了 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;
}
}
}
}
}
注意:在激活发布模式和 OpenMP 的情况下,我得到的结果与停用 OpenMP 的结果相同。调试模式和激活的 OpenMP 会得到不同的结果,停用的 OpenMP 会得到与发布相同的结果。
至少有两种可能性:
- 启用优化可能会导致编译器重新排序操作。与在调试模式下执行的顺序相比,这可能会在浮点计算中引入微小的差异,在调试模式下,不会发生操作重新排序。这可以解释调试和发布之间的数值差异,但不考虑调试模式下从一次运行到下一次运行之间的数值差异。
- 代码中存在与内存相关的错误,例如读取/写入超过数组边界、使用未初始化的变量、使用未分配的指针等。 尝试通过内存检查器(例如出色的 Valgrind)运行它以识别此类问题。与内存相关的错误可能是不确定行为的原因。
如果你在Windows上,那么Valgrind不可用(可惜),但你可以在这里寻找替代品列表。
为了详细说明我的评论,这是最有可能是您问题根源的代码:
#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
。现在只有一个关键区域,它位于代码的末尾。您也可以绕过它,但您必须引入其他数组。
要仔细检查的一件事是所有变量都已初始化。 很多时候,未优化的代码(调试模式)将初始化内存。
我会说调试中的变量初始化与发布中不存在的变量初始化。但是您的结果不会支持这一点(发布中的可靠结果)。
您的代码是否依赖于任何特定的偏移量或大小?调试版本会在某些分配周围放置保护字节。
会不会与浮点有关?
调试浮点堆栈不同于为提高效率而构建的版本。
看这里: http://thetweaker.wordpress.com/2009/08/28/debugrelease-numerical-differences/
几乎任何未定义的行为都可以解释这一点: 未初始化变量、恶意指针、同一对象的多次修改没有中间的序列点等。 事实上,结果有时是不可复制的,在某种程度上争论未初始化的变量,但它也可能由指针问题或边界错误。
请注意,优化可能会改变结果,尤其是在英特尔上。优化可以更改哪些中间值溢出到内存,并且如果您没有仔细使用括号,即使是评估顺序在表达式中。 (众所周知,在机器浮点中,(a +
b) + c) != a + (b + c)
。 结果仍然是确定性的:根据优化程度,你会得到不同的结果,但是对于任何一组优化标志,您都应该得到相同的结果。