Debug和Release之间的结果不同

Sin*_*rMJ 4 c++ openmp visual-studio-2010 release-mode debug-mode

我有一个问题,我的代码在将调试与发布进行比较时会返回不同的结果.我检查了两种模式都使用/ fp:exact,所以这不应该是问题.我对此的主要问题是完整的图像分析(它是一个图像理解项目)完全是确定性的,其中绝对没有任何随机性.

另一个问题是我的发布版本实际上总是返回相同的结果(图像为23.014),而debug返回22到23之间的一些随机值,这不应该是.我已经检查过它是否与线程相关,但算法中唯一的多线程部分会为调试和发布返回完全相同的结果.

还有什么可能发生在这里?

Update1:我现在发现的代码负责此行为:

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;
}
Run Code Online (Sandbox Code Playgroud)

Update2: 使用32位代码无法重现.虽然调试和释放代码总是会产生32位的相同值,但它仍然与64位版本不同,64位调试仍会返回一些完全随机的值.

Update3: 好的,我发现它肯定是由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;
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:在释放模式和OpenMP激活后,我得到与停用OpenMP相同的结果.调试模式和OpenMP激活获得不同的结果,OpenMP停用获得与Release相同的结果.

Nat*_*han 10

至少有两种可能性:

  1. 启用优化可能会导致编译器重新排序操作.与调试模式中执行的顺序相比,这可能会在浮点计算中引入较小的差异,在调试模式下不会发生操作重新排序.这可以解释调试和发布之间的数值差异,但并没有考虑从一个运行在调试模式下接下来的数值差异.
  2. 您的代码中存在与内存相关的错误,例如读取/写入数组的边界,使用未初始化的变量,使用未分配的指针等.尝试通过内存检查器(例如优秀的Valgrind)运行它找出这样的问题.与内存相关的错误可能会导致非确定性行为.

如果您在Windows上,那么Valgrind不可用(可惜),但您可以在这里查看备选列表.

  • 遇到非确定性行为时我检查的第一件事(我不使用随机数)是内存错误.如果没有正确的工具,他们会很难追踪(在我使用适当的内存调试工具之前,我常常花费数天时间找到它们). (2认同)

Tre*_*ent 6

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


Hri*_*iev 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)
    {
Run Code Online (Sandbox Code Playgroud)

last仅在再次读取它之前才将其赋值给它,因此lastprivate,如果您确实需要来自并行区域之外的最后一次迭代的值,则它很适合作为变量。否则就做吧private

访问bestHitcValrad,并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;
}
Run Code Online (Sandbox Code Playgroud)

请注意,默认情况下,所有变量(parallel for循环计数器和并行区域内定义的变量除外)都是共享的,即,shared除非您也应用该default(none)子句,否则该子句在您的情况下不执行任何操作。

您应该注意的另一件事是,在32位模式下,Visual Studio使用x87 FPU数学,而在64位模式下,默认情况下,它使用SSE数学。x87 FPU使用80位浮点精度(即使float仅涉及计算)进行中间计算,而SSE单元仅支持标准IEEE单精度和双精度。在32位x87 FPU代码中引入OpenMP或任何其他并行化技术意味着,在某些点上,中间值应转换回的单精度,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;
    }
}
Run Code Online (Sandbox Code Playgroud)

它只存储,让最好的砸在每个线程,然后在它计算的并行区域末端的参数的值radcValveneOffset基于最佳值。现在只有一个关键区域,它位于代码的末尾。您也可以解决它,但是您必须引入其他阵列。