【问题标题】:Different results between Debug and ReleaseDebug 和 Release 的结果不同
【发布时间】:2012-08-14 14:14:17
【问题描述】:

我的问题是在比较调试和发布时我的代码返回不同的结果。我检查了两种模式都使用 /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 位代码重现。虽然 debug 和 release 代码在 32bit 下总是会产生相同的值,但它仍然与 64bit release 版本不同,并且 64bit debug 仍然返回一些完全随机的值。

更新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;
                }
            }
        }
    }
}

注意:在释放模式和 OpenMP 激活的情况下,我得到与停用 OpenMP 相同的结果。 Debug 模式和 OpenMP 激活得到不同的结果,OpenMP deactivated 得到与 Release 相同的结果。

【问题讨论】:

  • 如果我们看到一些代码,我们可能会提供更多帮助。一般来说,我的猜测是您在普通编译器可以正确理解的地方使用了松散的语法,但调试器却没有。
  • 使用 valgrind 检查您是否有可能导致不确定行为的内存损坏。
  • 有趣。通常的 Heisenbug 情况是调试得到更可靠的结果。
  • 闻起来像未定义的行为......
  • 发布和调试只是不同的项目选项集——您可以一一更改选项,直到找到使发布输出与调试输出匹配的选项。但是我们没有足够的信息来告诉你发生了什么。打印出中间输出,分而治之... 8 - )

标签: c++ visual-studio-2010 openmp release-mode debug-mode


【解决方案1】:

至少有两种可能:

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

如果您使用的是 Windows,则 Valgrind 不可用(遗憾),但您可以查看 here 以获取替代列表。

【讨论】:

  • 我现在在发布模式下完全关闭了优化,现在我在发布模式下得到了相同的随机结果。为什么完全优化会产生确定性的结果,而 Debug 会给我一些随机返回值?
  • 遇到非确定性行为(而且我没有使用随机数)时,我首先检查的是内存错误。如果没有合适的工具,很难找到它们(在我拥有适当的内存调试工具之前,我曾经花费数天时间找到它们)。
  • @AntonRoth 通常情况正好相反,但优化器可能会消除某些计算,因为它“知道”结果,而没有优化它不会。如果这些计算在某处使用了未初始化的值...
  • @AntonRoth 另一种可能性是某些代码行为不良并且具有意外的副作用。重新排序操作可能不会消除副作用,但它可能会将它们移动到计算中不会损害结果的点。
  • 我现在在 Microsoft 的 ApplicationVerifier 中运行该应用程序,它显示 0 个错误,0 个警告。有趣的事情:再次以 32 位运行它会产生不同的值 (23.009),但这一次对于调试和发布模式都是确定性的。
【解决方案2】:

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

#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。现在只有一个临界区,它位于代码的末尾。你也可以绕过它,但你必须引入额外的数组。

【讨论】:

  • 谢谢,最后声明为私有,现在我在发布和调试模式之间得到相同的结果!
  • @AntonRoth,您是否还添加了关键部分?没有它们,您无法保证不会发生数据竞争。
  • 是的,我做到了,但是尝试了 20 次,结果却没有任何区别。实际上,#pragma omp critical 的性能比一开始就使用单线程要差得多。
  • 是的,临界区增加了同步开销。您可以做的只是将lastrck 的值存储在共享数组中的每个线程中(在并行区域的末尾执行此操作;数组每个线程应该有一个元素;使bestHist私有),然后在并行区域之外检查数组并根据来自具有最佳bestHit值的线程的值计算radcValveneOffset .
  • @AntonRoth,我添加了一个示例代码,说明如何在每次迭代时同步对共享变量的访问。请注意,“20 次尝试,结果从未有任何区别”与“它永远不会给出不同的结果”不同。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

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

    可能是浮点相关的吗?

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

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

    【讨论】:

      【解决方案5】:

      几乎任何未定义的行为都可以解释这一点:未初始化 变量、流氓指针、同一对象的多次修改 没有中间的序列点等。事实上, 结果有时无法再现 未初始化的变量,但它也可能因指针问题或 界限错误。

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

      【讨论】:

        猜你喜欢
        • 2014-11-01
        • 2013-03-24
        • 2018-10-18
        • 2023-03-31
        • 2014-06-15
        • 1970-01-01
        • 2015-06-14
        • 2020-12-30
        • 2023-01-11
        相关资源
        最近更新 更多