【问题标题】:Illogical benchmarking?不合逻辑的基准测试?
【发布时间】:2009-03-23 00:03:45
【问题描述】:

我目睹了以下奇怪的行为。我有两个功能几乎相同 - 它们测量执行某个操作所需的周期数。在一个函数中,在循环内我增加了一个变量;在其他什么都没有发生。这些变量是易变的,因此它们不会被优化掉。这些是函数:

unsigned int _osm_iterations=5000;

double osm_operation_time(){
    // volatile is used so that j will not be optimized, and ++ operation
    // will be done in each loop
    volatile unsigned int j=0;
    volatile unsigned int i;
    tsc_counter_t start_t, end_t;
    start_t = tsc_readCycles_C();
    for (i=0; i<_osm_iterations; i++){
       ++j;
    }
    end_t = tsc_readCycles_C();
    if (tsc_C2CI(start_t) ==0 || tsc_C2CI(end_t) ==0 || tsc_C2CI(start_t) >= tsc_C2CI(end_t))
         return -1;
    return (tsc_C2CI(end_t)-tsc_C2CI(start_t))/_osm_iterations;
}

double osm_empty_time(){
    volatile unsigned int i;
    volatile unsigned int j=0;
    tsc_counter_t start_t, end_t;
    start_t = tsc_readCycles_C();
    for (i=0; i<_osm_iterations; i++){
        ;
    }
    end_t = tsc_readCycles_C();
    if (tsc_C2CI(start_t) ==0 || tsc_C2CI(end_t) ==0 || tsc_C2CI(start_t) >= tsc_C2CI(end_t))
        return -1;
    return (tsc_C2CI(end_t)-tsc_C2CI(start_t))/_osm_iterations;
}

那里有一些非标准功能,但我相信你会管理。

问题是,第一个函数返回 4,而第二个函数(据说做的更少)返回 6,尽管第二个显然比第一个做的少一个。

这对任何人都有意义吗?

实际上,我制作了第一个函数,这样我就可以减少测量第二个函数的循环开销。你知道怎么做吗(因为这种方法并没有真正削减它)?

我使用的是 Ubuntu(我认为是 64 位)。

非常感谢。

【问题讨论】:

  • 变量 j 似乎在两个循环中都增加了
  • 是的,这只是一个愚蠢的错误......
  • _osm_iterations 似乎从未被定义过,它是全球性的吗?如果有,它是如何实例化的?
  • 它是全局的:unsigned int _osm_iterations=5000;
  • 为什么说第二个功能做得更多?看起来它实际上做得更少。

标签: c optimization loops benchmarking


【解决方案1】:

我可以在这里看到几件事。一是两个循环的代码看起来相同。其次,编译器可能会意识到变量i 和变量j 将始终具有相同的值并优化其中一个。您应该查看生成的程序集,看看到底发生了什么。

另一个理论是对循环内部主体的更改影响了代码的可缓存性——这可能已经将它移动到缓存行或其他一些东西上。

由于代码如此琐碎,您可能会发现很难获得准确的时序值,即使您进行 5000 次迭代,您可能会发现时间在您使用的时序代码的误差范围内。一台现代计算机可能可以在不到一毫秒的时间内运行它 - 也许您应该增加迭代次数?

要在 gcc 中查看生成的程序集,specify the -S compiler option:

问:如何查看汇编代码 由 GCC 生成?

问:如何在我可以创建的地方创建文件 查看 C 代码及其程序集 一起翻译?

A:使用 -S(注意:大写 S)开关 到 GCC,它会发出程序集 将代码添加到扩展名为 .s 的文件中。 例如以下命令:

gcc -O2 -S -c foo.c

会留下生成的汇编代码 在文件 foo.s 上。

如果你想一起看C代码 随着它被转换成的程序集, 使用这样的命令行:

gcc -c -g -Wa,-a,-ad [其他 GCC 选项] foo.c > foo.lst

这将输出组合 C/程序集列表到文件 foo.lst.

【讨论】:

  • 第一件事只是复制错误。关于第二个 - 我真的不知道该怎么做......有什么提示吗?
  • 还有 - 如果它们被定义为 volatile,为什么还要优化它们?
  • 我真的不知道它是否有,但由于它们是堆栈分配的局部变量,你永远不会获取它们的地址或其他奇怪的东西,也许编译器可以告诉它们永远被一些外部来源所触动
  • 通常你的编译器可以选择查看生成的程序集——我不知道你的会是什么
  • 更多的迭代不能解决它:)
【解决方案2】:

有时很难猜测这类事情,尤其是由于迭代次数很少。不过,可能发生的一件事是,增量可以在一个空闲的整数执行单元上执行,从而获得一定程度的并行性,因为它与 i 的值没有任何关系。

由于您提到这是 64 位操作系统,因此几乎可以肯定所有这些值都在寄存器中,因为 x86_64 架构中有更多寄存器。除此之外,我会说执行更多的迭代,看看结果有多稳定。

【讨论】:

    【解决方案3】:

    如果您真的想测试一段代码的操作(在这种情况下为"j++;"),您实际上最好执行以下操作:

    1/ 在两个单独的可执行文件中执行此操作,因为可执行文件中的位置可能会影响代码。

    2/ 确保使用 CPU 时间而不是经过的时间(我不确定 "tsc_readCycles_C()" 给了你什么)。这是为了避免 CPU 加载其他任务而导致错误结果。

    3/ 关闭编译器优化(例如,"gcc -O0")以确保 gcc 不会放入任何可能会影响结果的花哨的东西。

    4/如果你使用实际结果,例如放置,则无需担心volatile

    printf ("%d\n",j);
    

    在循环之后,或者:

    FILE *fx = fopen ("/dev/null","w");
    fprintf (fx, "%d\n", j);
    fclose (fx);
    

    如果您根本不想要任何输出。我不记得 volatile 是对编译器的建议还是强制执行。

    5/ 5,000 次的迭代似乎有点偏低,“噪音”可能会影响读数。也许更高的价值会更好。如果您正在计时一段较大的代码并且您刚刚将 "j++;" 作为占位符包含在内,这可能不是问题。

    【讨论】:

      【解决方案4】:

      当我运行类似的测试时,我通常:

      1. 确保以至少几秒为单位测量时间,最好是(小)几十秒。
      2. 让程序运行一次,调用第一个函数,然后调用第二个函数,然后再调用第一个函数,然后再调用第二个函数,以此类推,看看是否存在奇怪的缓存预热问题。
      3. 多次运行该程序,查看运行时间的稳定性。

      我仍然无法解释您观察到的结果,但如果您确定您的函数已正确识别(鉴于之前存在复制粘贴错误,情况并非不言而喻,因为例如),然后查看汇编器输出是剩下的主要选项。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-11-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多