为什么printf()允许通过指针传递double类型?

10
一对printf()调试语句揭示了一个指向我正在传递的double类型指针,在接收端解引用时得到了不同的值 - 但仅在Microsoft Visual Studio(版本9.0)下出现。步骤非常简单:
    double rho=0;       /* distance from the Earth */
    /* ... */
    for (pass = 0; pass < 2; pass++) {
        /* ... */
        rho = sqrt(rsn*rsn+rp*rp-2*rsn*rp*cpsi*cos(ll));
        printf("\nrho from sqrt(): %f\n", rho);
        /* ... */
    }
    /* ... */
    cir_sky (np, lpd, psi, rp, &rho, lam, bet, lsn, rsn, op);
    /* ... */
}
/* ... */
static void
cir_sky (
/* ... */
double *rho,        /* dist from earth: in as geo, back as geo or topo */
/* ... */)
{
    /* ... */
    printf("\nDEBUG1: *rho=%f\n", *rho);

整个C文件在这里:

https://github.com/brandon-rhodes/pyephem/blob/9cd81a8a7624b447429b6fd8fe9ee0d324991c3f/libastro-3.7.7/circum.c#L366

我本以为第一个printf()显示的值应该与第二个相同,因为传递一个指向double的指针不应该导致不同的值。在GCC下,它们实际上总是相同的。在Visual Studio 32位编译下,它们始终相同。但是当这段代码在64位架构下使用Visual Studio编译时,两个double值是不同的!

https://ci.appveyor.com/project/brandon-rhodes/pyephem/build/1.0.18/job/4xu7abnl9vx3n770#L573

rho from sqrt(): 0.029624

DEBUG1: *rho=0.000171

这让人感到不安。我想知道:在计算 rho 的代码和最终传递指针的代码之间,是否存在错误的指针算术运算导致值被破坏?因此,在 cir_sky() 调用的上方添加了一个最后的 printf(),以查看该点是否已经改变或者在调用本身的过程中被改变:

    printf("\nrho about to be sent: %f\n", rho);
    cir_sky (np, lpd, psi, rp, &rho, lam, bet, lsn, rsn, op);

以下是整个文件中的那行代码:

https://github.com/brandon-rhodes/pyephem/blob/28ba4bee9ec84f58cfffabeda87cc01e972c86f6/libastro-3.7.7/circum.c#L382

你猜怎么着?

添加printf()修复了这个bug - 传递给rho的指针现在可以被解引用为正确的值!

如下图所示:

https://ci.appveyor.com/project/brandon-rhodes/pyephem/build/1.0.19/job/s3nh90sk88cpn2ee#L567

rho from sqrt(): 0.029624

rho about to be sent: 0.029624

DEBUG1: *rho=0.029624

我感到困惑。
我在这里遇到了C标准的什么边缘情况?为什么仅仅在函数的顶层作用域中使用rho的值就能够强制微软编译器正确地保留它的值?问题在于rho在一个块内既被设置又被使用,而Visual Studio由于C标准的某个怪癖不会在该块之外保留其值吗?我从未完全理解过这个问题。
您可以在上面的AppVeyor链接中查看整个构建输出。如果问题可能是Visual Studio的调用或编译选项,那么该C文件的特定编译步骤如下:
C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\Bin\amd64\cl.exe /c /nologo /Ox /MD /W3 /GS- /DNDEBUG -Ilibastro-3.7.7 -IC:\Python27-x64\include -IC:\Python27-x64\PC /Tclibastro-3.7.7\circum.c /Fobuild\temp.win-amd64-2.7\Release\libastro-3.7.7\circum.obj
circum.c
libastro-3.7.7\circum.c(126) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data 
libastro-3.7.7\circum.c(127) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data
libastro-3.7.7\circum.c(139) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data 
libastro-3.7.7\circum.c(140) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data 
libastro-3.7.7\circum.c(295) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data 
libastro-3.7.7\circum.c(296) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data
libastro-3.7.7\circum.c(729) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data 
libastro-3.7.7\circum.c(730) : warning C4244: '=' : conversion from 'double' to 'float', possible loss of data

没有一个警告是与这个谜题相关的代码,即使有,也只是意味着浮点值可能会变得不那么精确(从约15位小数精度到7位),而不是完全改变。以下是两次编译和测试运行的输出,第一次失败,第二次因为printf()而成功:

https://ci.appveyor.com/project/brandon-rhodes/pyephem/build/1.0.18/job/4xu7abnl9vx3n770

https://ci.appveyor.com/project/brandon-rhodes/pyephem/build/1.0.19/job/s3nh90sk88cpn2ee

根据AppVeyor,两者都是完全相同的架构:

Environment: PYTHON=C:\Python27-x64, PYTHON_VERSION=2.7.x, PYTHON_ARCH=64, WINDOWS_SDK_VERSION=v7.0

4
你为什么要给这个打上 python 的标签? - BrenBarn
4
要求调试帮助的问题(“为什么这段代码不起作用?”)必须包含期望的行为、具体问题或错误以及在问题本身中复制所需最短代码的步骤。没有清晰的问题陈述对其他读者没有用处。参见:如何创建一个简洁、完整和可验证的示例 - user3386109
2
你的代码中可能存在内存损坏吗?内存损坏常常导致类似这样的非确定行为。值得通过类似 valgrind 的工具运行它。 - kaylum
2
有没有办法缩小代码的大小并消除与Python相关的链接,同时仍然能够重现这个问题? - rcgldr
1
我的猜测是有一些被取消引用的指针正在被修改,这对周围的内存产生了不可预测的影响。没有“printf”,这种修改会影响“rho”。有了“printf”,这种修改会影响分配给打印的内存,并且不会影响rho。 - jakevdp
显示剩余11条评论
2个回答

1

我快速浏览了这段代码,并没有发现任何错误或问题。但是,当 printf 解决了问题时,这意味着存在一些不确定性。让我们分析可能的原因:

  1. 并发性 - 数据竞争:最常见的问题,但你说它是单线程的。
  2. 未初始化的内存:这里初始化了rho,但是,其他地方可能有未初始化的内存,导致出现问题。我建议在Linux上运行valgrind以及AdressSanitizer和其他的sanitizers(在Windows的clang和gcc上也应该有),看看它们是否能找到问题。
  3. 野指针和其他越界访问:代码中没有看到这种情况,但是它调用了其他函数。同样,运行valgrind和sanitizers。
  4. 如果前面的步骤都没有发现问题,那么下一个最有可能的原因是MSVC的bug。MSVC以某些复杂代码而闻名于世,并且这个代码有点复杂。我曾经多次重排代码只是为了让MSVC满意。有时关闭优化有帮助,有时没有。对于尝试不同的编译器选项也是如此。有时会有更新/补丁有帮助,有时没有。下一个版本的MSVC也是如此。我建议在调试器中查看反汇编代码,但是你说你无法访问Windows机器。在这里最好的选择是尝试简化代码-使函数更小,减少参数数量。
  5. 还有其他可能的原因。例如,也许由于某种原因堆栈被搞乱了-也许是与Python运行时交互时发生的。尝试将其构建并作为“常规”C代码运行,而不是Python扩展程序。消除对其他函数的调用(如果它影响计算,那就没关系,你只是想找出问题)。

无论如何,我建议您获取一台Windows机器并进行调试。根据我的经验,这是解决此类问题的最佳方法。


0

这是(错误的)优化效果吗?

关闭任何优化(DEBUG?),看看是否会出现相同的效果。

当然,如果您发现是优化器的问题,那么您只能做一些事情来欺骗它,例如一个什么也不做的sprintf。

此外,您的printf也可以打印指针(“%16x”,(long)&rho),虽然我认为这不是错误,但只是作为一个理智条款,以防我们遗漏了什么。此外,大多数带有随机位的双精度数的结果通常会落在E+/-317范围内,因此0.000171的结果有点太合理,不能完全怀疑。


这些都是很好的建议,但并不是问题的答案。更适合作为评论。 - kaylum
2
嗯,我还没有50r,所以这是我能做的最好了。 - Brian Carcich
@brian_carcich 好的,说得对。但更好的做法是获得更多的声望点数,直到你能够发表评论为止。规则存在是有原因的。通过发布非答案回复来规避规则并不是应该做的事情。 - kaylum
2
这似乎是一个合理的答案来尝试追踪问题。如果问题可以在调试版本中复现,那么如果VS的调试器包括观察点类型的断点,那么可以使用它来检查是否有任何写入rho的操作,以查看是什么破坏了它。 - rcgldr

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