调试和发布之间的结果不同



我遇到了一个问题,即我的代码在比较调试与发布时返回不同的结果。我检查了两种模式是否都使用/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 会得到与发布相同的结果。

至少有两种可能性:

  1. 启用优化可能会导致编译器重新排序操作。与在调试模式下执行的顺序相比,这可能会在浮点计算中引入微小的差异,在调试模式下,不会发生操作重新排序。这可以解释调试和发布之间的数值差异,但不考虑调试模式下从一次运行到下一次运行之间的数值差异。
  2. 代码中存在与内存相关的错误,例如读取/写入超过数组边界、使用未初始化的变量、使用未分配的指针等。 尝试通过内存检查器(例如出色的 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.

bestHitcValradveneOffset的访问应由关键区域同步:

#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;
    }
}

只存储在每个线程中给出最佳命中的参数值,然后在并行区域的末尾,它根据最佳值计算radcValveneOffset。现在只有一个关键区域,它位于代码的末尾。您也可以绕过它,但您必须引入其他数组。

要仔细检查的一件事是所有变量都已初始化。 很多时候,未优化的代码(调试模式)将初始化内存。

我会说调试中的变量初始化与发布中不存在的变量初始化。但是您的结果不会支持这一点(发布中的可靠结果)。

您的代码是否依赖于任何特定的偏移量或大小?调试版本会在某些分配周围放置保护字节。

会不会与浮点有关?

调试浮点堆栈不同于为提高效率而构建的版本。

看这里: http://thetweaker.wordpress.com/2009/08/28/debugrelease-numerical-differences/

几乎任何未定义的行为都可以解释这一点: 未初始化变量、恶意指针、同一对象的多次修改没有中间的序列点等。 事实上,结果有时是不可复制的,在某种程度上争论未初始化的变量,但它也可能由指针问题或边界错误。

请注意,优化可能会改变结果,尤其是在英特尔上。优化可以更改哪些中间值溢出到内存,并且如果您没有仔细使用括号,即使是评估顺序在表达式中。 (众所周知,在机器浮点中,(a + b) + c) != a + (b + c)。 结果仍然是确定性的:根据优化程度,你会得到不同的结果,但是对于任何一组优化标志,您都应该得到相同的结果。

相关内容

  • 没有找到相关文章

最新更新