【问题标题】:Can you reproduce or explain this Visual C++ bug with ctime?你能用 ctime 重现或解释这个 Visual C++ 错误吗?
【发布时间】:2014-10-04 02:55:18
【问题描述】:

当在发布模式下使用 Visual Studio Professional 2013 Update 3(32 位和 64 位选项)编译时,无论 N 的值如何,此 code example 都将输出 time: 0

#include <iostream>
#include <functional>
#include <ctime>

using namespace std;

void bar(int i, int& x, int& y)  {x = i%13; y = i%23;}

int g(int N = 1E9) {   
  int x, y;
  int r = 0;

  for (int i = 1; i <= N; ++i) {
    bar(i, x, y);
    r += x+y;
  }

  return r;
}

int main()
{
    auto t0 = clock();
    auto r  = g();
    auto t1 = clock();

    cout << r << "  time: " << t1-t0 << endl;

    return 0;
}

在 rextester.com 上使用 gcc、clang 和其他版本的 vc++ 进行测试时,它的行为正确并输出大于零的time。任何线索这里发生了什么?

我注意到内联 g() 函数可以恢复正确的行为,但更改 t0rt1 的声明和初始化顺序不会。

【问题讨论】:

  • 执行操作的时间可能低于 Windows 在调用 clock() 时使用的计时器的分辨率。我最近在这里写了一个答案,可能会让您了解在 Windows 上使用更高分辨率的计时器:stackoverflow.com/questions/25954602/ctimespan-always-gets-zero
  • 这是一个优化问题(编译器认为调用 g 没有副作用),因此它实际上在调用 g 之前将两个调用都设置为时钟。您应该可以通过将 auto r = g(); 更改为 volatile auto r = g(); 来解决此问题
  • @MichaelPetch 你的volatile 将阻止g 在第二个clock 之后移动,但你还需要一些东西来阻止它在第一个clock 之前移动,例如:@987654338 @.
  • @Paul 不,抱歉。将g() 视为计算 2+2 的东西。它可以在编译时计算,也可以在 main 开始时计算,或者在 2 个clock 之间计算,你所知道的是它在存储到 r 之前已经计算过了。为了澄清事情,像volatile auto r = g(); 这样的声明不必保留在一个整体中。编译器首先将其拆分为auto tmp=g(); volatile auto r=tmp;,并且第一部分可以随心所欲地移动,因为它没有副作用并且不依赖任何东西。
  • @Paul 对。请注意,定义n 的这一行可以在第一个clock 之前或之后,没关系。读取n 的值是一个副作用,所以它不能与clock 通勤。由于g 取决于该值,因此g 在读取后必须保留。

标签: c++ visual-studio-2013 std


【解决方案1】:

如果您使用调试器查看反汇编窗口,您可以看到生成的代码。对于发布模式下的 VS2012 express,你会得到这个:

00AF1310  push        edi  
    auto t0 = clock();
00AF1311  call        dword ptr ds:[0AF30E0h]  
00AF1317  mov         edi,eax  
    auto r  = g();
    auto t1 = clock();
00AF1319  call        dword ptr ds:[0AF30E0h]  

    cout << r << "  time: " << t1-t0 << endl;
00AF131F  push        dword ptr ds:[0AF3040h]  
00AF1325  sub         eax,edi  
00AF1327  push        eax  
00AF1328  call        g (0AF1270h)  
00AF132D  mov         ecx,dword ptr ds:[0AF3058h]  
00AF1333  push        eax  
00AF1334  call        dword ptr ds:[0AF3030h]  
00AF133A  mov         ecx,eax  
00AF133C  call        std::operator<<<std::char_traits<char> > (0AF17F0h)  
00AF1341  mov         ecx,eax  
00AF1343  call        dword ptr ds:[0AF302Ch]  
00AF1349  mov         ecx,eax  
00AF134B  call        dword ptr ds:[0AF3034h]  

从汇编的前 4 行可以看出,对 clock (ds:[0AF30E0h]) 的两次调用发生在对 g 的调用之前。所以在这种情况下,g 需要多长时间并不重要,结果只会显示这两个连续调用之间的时间。

似乎 VS 已确定 g 没有任何会影响 clock 的副作用,因此可以安全地移动调用。

正如 Michael Petch 在 cmets 中指出的那样,将 volatile 添加到 r 的声明中将阻止编译器移动调用。

【讨论】:

  • r 标记为volatile 可能足以让编译器改变这种行为。我希望这会起作用volatile auto r = g();
  • 合法吗?按照这个推理,几乎所有的计算都不会影响clock,所以它可以被编译器移动到任何地方,但这将是一种疯狂的行为。
  • @MichaelPetch 你是对的 - 是的 - 我会在答案中添加它。
  • @MichaelPetch 有一个合理的预期,即无论是否优化,用户观察到的结果都是相同的 - 事实上,有这样一个要求。我刚刚查阅了 The C++ Programming Language, 4th Edition 并在第 225 页上写道:编译器可以重新排序代码以提高性能,只要结果与简单顺序的结果相同执行
  • 是的,将您遇到的问题称为优化错误是合理的。 Volatile 关键字的存在是为了向编译器提示不应该对变量(值、指针等)进行此类优化。使用优化会带来这些陷阱。这通常是大多数编译器允许您关闭某些优化或设置不同级别的原因。
猜你喜欢
  • 1970-01-01
  • 2013-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-21
  • 1970-01-01
  • 1970-01-01
  • 2016-12-30
相关资源
最近更新 更多