Debug与Release模式下的结果不同。

5
我有一个问题,我的代码在调试和发布时返回不同的结果。我检查了两种模式都使用/fp:precise,所以这不应该是问题。我对此的主要问题是完整的图像分析(它是一个图像理解项目)是完全确定的,其中绝对没有任何随机性。
另一个问题是,我的发布版本实际上始终返回相同的结果(图像为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引起的。当我禁用它时,它就可以正常工作(调试和发布都使用相同的代码,并且都启用了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并启用Debug模式会得到不同的结果,关闭OpenMP会得到与Release相同的结果。

如果我们看到一些代码,或许可以提供更多的帮助。总的来说,我猜测你在某个地方使用了普通编译器无法理解但调试器能够理解的宽松语法。 - rsegal
1
使用Valgrind检查是否存在可能导致非确定性行为的内存损坏。 - Thomas W.
1
有趣。通常的Heisenbug情况是调试会得到更可靠的结果。 - dmckee --- ex-moderator kitten
1
闻起来像是未定义的行为... - BoBTFish
@AlexanderChertov:添加中间输出很可能会改变结果,因为它会强制编译器对操作进行排序。插入printf语句,问题可能会消失;将其删除,问题就会重新出现。 - Nathan
显示剩余4条评论
5个回答

13

至少有两种可能性:

  1. 打开优化选项可能会导致编译器重新排序操作。与在不发生操作重新排序的调试模式下执行的顺序相比,这可能会引入浮点计算中的小差异。这 可能 解释了在调试和发布之间数值差异的原因,但没有解释在调试模式下一次运行与下一次运行之间的数值差异。
  2. 您的代码存在内存相关错误,例如读取/写入数组边界之外的内容、使用未初始化的变量、使用未分配的指针等。尝试通过内存检查器(如优秀的 Valgrind)运行它,以识别此类问题。与内存相关的错误 可能 解释了非确定性行为。

如果您正在使用 Windows,则无法使用 Valgrind(遗憾),但您可以查看此处获取替代方案列表。


1
我现在已经完全关闭了Release模式下的优化,但是现在我在Release模式下得到了与Debug模式下相同的随机结果。为什么完全优化会导致确定性结果,而Debug却给我一些随机返回值? - SinisterMJ
2
当我遇到非确定性行为(而且我没有使用随机数)时,我首先检查的是内存错误。如果没有正确的工具,追踪它们会非常麻烦(在我拥有适当的内存调试工具之前,我曾经花费数天时间找到它们)。 - Nathan
1
@AntonRoth 通常情况下是相反的,但是优化器有可能会消除某些计算,因为它“知道”结果,在没有优化的情况下则不会。而如果这些计算在某处使用了未初始化的值... - James Kanze
@AntonRoth 另一个可能性是某些代码表现不佳并具有意外的副作用。重新排列操作可能不会消除副作用,但它可以将它们移动到计算中对结果不会产生负面影响的位置。 - Nathan
我现在在Microsoft的ApplicationVerifier中运行了该应用程序,它显示0个错误,0个警告。有趣的是:使用32位再次运行会得到不同的值(23.009),但这次对于调试和发布模式都是确定性的。 - SinisterMJ
@AntonRoth,你发布的代码中肯定没有任何东西会导致在重复使用相同输入时调试模式下出现不确定性。一定是循环外部有其他原因导致了不确定性。我建议验证所有调用中的输入是否相同。从调试模式到优化模式,编译器可能会展开你的循环,这将导致w*加操作被重新排序并产生一些浮点值差异,但不会导致不确定性。 - Nathan

7

为了详细说明我的评论,这段代码很可能是您问题的根源:

#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。现在只有一个关键区域,在代码结尾处。你也可以绕过它,但你需要引入额外的数组。

谢谢,将last声明为私有变量就可以了,现在我在发布模式和调试模式下得到了相同的结果! - SinisterMJ
@AntonRoth,你是否也添加了关键部分?没有它们,你无法保证不会发生数据竞争。 - Hristo Iliev
是的,我尝试过,但是尝试了20次后,结果并没有任何改变。实际上,在使用#pragma omp critical时,性能比一开始就单线程要差得多。 - SinisterMJ
是的,关键部分会增加同步开销。您可以做的是仅在共享数组中存储每个线程中给出最佳命中的lastrck的值(在并行区域的末尾执行;该数组应该有一个元素每个线程;使bestHist为私有),然后在并行区域之外检查数组并根据具有最佳bestHit值的线程的值计算radcValveneOffset - Hristo Iliev
@AntonRoth,我已经添加了一个示例代码,展示如何在每次迭代中避免同步访问共享变量。请注意,“20次尝试中从未对结果产生影响”与“永远不会产生不同的结果”是不同的。 - Hristo Iliev
啊,太好了。我一直使用手动线程,对OpenMP线程不是很熟悉。非常感谢! - SinisterMJ

6

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


2
我本来会说是debug中的变量初始化与发布版本中没有所致。但您的结果并不支持这个(在发布版本中有可靠结果)。
您的代码是否依赖于特定的偏移量或大小?调试构建将在某些分配周围放置保护字节。
可能与浮点数有关吗?
调试浮点堆栈与发布版不同,后者更加高效。
请看这里:http://thetweaker.wordpress.com/2009/08/28/debugrelease-numerical-differences/

2
几乎任何未定义的行为都可以解释这一点:未初始化变量、不受限制的指针、在没有中介序列点的情况下多次修改同一对象等等。有时候结果无法重现,这说明可能存在未初始化的变量,但也可能是由于指针问题或边界错误引起的。
请注意,优化会改变结果,特别是在英特尔上。优化可能会改变中间值溢出到内存的方式,如果您没有仔细使用括号,甚至可能会更改表达式的评估顺序。(正如我们所知,在机器浮点数中,(a + b)+ c)!= a +(b + c)。)尽管如此,结果应该是确定性的:您将根据优化程度而获得不同的结果,但对于任何一组优化标志,您应该获得相同的结果。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接