【问题标题】:Efficiently treating functions in C/C++ called a million times有效处理 C/C++ 中调用一百万次的函数
【发布时间】:2015-06-30 18:08:17
【问题描述】:

对于编程问题,我是 C 和 stackoverflow 的新手。我已经对我的问题进行了一些谷歌搜索,但我无法直接找到解决这个问题的信息。但是,我也可能对这个主题太陌生,以至于我不确定哪些术语甚至适合搜索。如果这是一个常见问题,我深表歉意。

我的程序正在执行科学计算。特别是,它主要涉及获取给定的坐标和给定的力,并根据一系列计算更新所有内容。为了获得结果,我将所有事情都运行了大约一百万次,所以我关心的是让它尽可能高效。特别是我发现我有 3 种类型的变量

  1. 恒定不变的变量
  2. 从迭代到迭代的临时变量 并且只是在计算中用作占位符
  3. 每次迭代都会更改然后执行的数据 到下一个

声明这些不同变量的最有效方法是什么?在我的天真中,我很想将所有东西都声明为全局变量,我很确定这对于 (1) 和 (3) 类型的变量是有意义的。但是对于类型(2)我不确定。如果我调用一个函数一百万次,并且每次调用它都会初始化一个临时变量,这是否比我有一个全局临时值而浪费更多时间?

【问题讨论】:

  • 在我看来,您可以自己尝试这两个选项并对结果进行基准测试,您会得到明确的答案。
  • 发布一些真实的代码怎么样?
  • 无论您面临什么性能问题,很可能都不在于您如何存储变量,也不在于您如何调用函数。如果您在代码中使用分析器并发现您的每一行(例如 1000 行代码)大约需要总运行时间的千分之一,就会出现这种情况。但很可能情况并非如此。因此,请使用分析器来查明瓶颈,并修复仅该部分
  • 尝试将变量的生命周期限制为“刚好够用”
  • 建议:你可能专注于错误的事情。您声明变量的方式/位置可能会对性能产生影响,但在您描述的情况下不太可能成为主要的性能因素。除非你有特定的理由认为它是。

标签: c++ c algorithm optimization


【解决方案1】:

如果你在循环中调用你的函数,粗略地说,将变量存储为全局或本地不应该改变任何东西。实际上,汇编代码很可能是相同的 (sub esp, SomeValue) ;唯一的区别是 SomeValue 可能会有所改变;这不会改变任何延迟;到时钟周期精度。

实际上,将变量设置为全局变量甚至会使您的程序运行速度变慢,因为编译器将无法理解您的代码并对其进行优化。

因此,如果您在循环中调用您的函数,请不要为全局与本地而烦恼,只需采用通常的方式:尽可能将变量设为本地。

但是,如果你的百万调用是递归调用,那么内存是稀缺的,你应该尽力去保存它,然后应该走全局的方式。

无论如何,您很可能有更好的优化等待在您的代码中进行!

【讨论】:

  • 考虑优化甚至可以将局部变量变成全局变量或相反(甚至为它保留一个 cpu 寄存器,导致一个非内存变量)
【解决方案2】:

如果我调用一个函数一百万次并且每次调用它 初始化一个临时变量,这是否比我浪费更多时间 有一个它改变的全局温度值吗?

不,甚至可能情况正好相反。在堆栈上创建一个临时变量。

分配堆栈变量没有性能成本
调用函数时,无论如何都必须移动堆栈指针,以便为函数参数和其他东西腾出空间。现在如果必须分配一个额外的变量,所有要做的就是增加堆栈指针的移动量。
与 Java 不同,您可以不初始化变量,并在需要时直接存储最终值,这样就没有初始化成本。

Locality of reference
当您使用堆栈变量时,它也更好。这意味着函数使用的值彼此更接近,这有助于处理器更有效地缓存数据。缓存通常以 64 字节的行组织,这意味着彼此相邻的两个 32 位值比两个分散的值更有效。
发生分页时,空间局部性尤其重要,因为从永久存储中加载页面具有巨大的性能成本。因此,最好将特定时间所需的所有变量都存储在一起,以便它们适合一页。

【讨论】:

  • 谢谢。我对编程知之甚少,无法理解堆栈细节,但我会相信你的话并在函数中初始化它们
  • 感谢您相信我 :-),但请记住,优化总是必须在一切完成后进行测试,以了解它们有什么不同(如果有的话)。
  • 分配堆栈变量没有性能成本,但初始化它。如果他要初始化几个局部变量,但在调用函数之前只初始化一次(生命中的全局变量,但范围内的局部变量),他可以选择在内部将它们声明为static函数体。他将初始化它们一次并多次使用,而不会有被其他地方看到的风险。事实上,这就是默认不初始化局部变量的原因。
  • 处理器不缓存基于空间位置的数据。它使用哈希表,因此可以传播数据。 时间的局部性什么数据保留在缓存中。如果您在上次使用后不久访问相同的数据,它很可能仍然存在于缓存中而不是不存在。缓存通常使用一些 LRU 算法(最近最少使用)来替换缓存中的数据。
  • 顺便说一下,空间局部性在指令缓存中很重要。如果循环的代码被完全缓存,您将使用缓存来执行所有循环。这与 cpu 执行指令的连续访问有关。但数据不会遇到同样的问题。
【解决方案3】:

类型 1 变量应该是全局变量

const double ABC = 1.234567;

const 对性能很重要。

类型 3 不能是全局的。它们需要在每个函数调用中传递

void f1(double x)
{
    x = x + ABC;
    f2(x);
}

如果您需要 f2 来更改 x 并获取更改后的值,请改用引用:

void f1(double& x)
{
    x = x + ABC;
    f2(x);
}

类型 2 应该只是函数中的局部:

void f1(double x)
{
    double y;
    x = x + ABC;
    f2(x);
}

请务必检查编译器优化标志以获得最佳性能。

【讨论】:

  • Type 1 变量是什么意思? 类型 2 发生了什么?如果您从不使用对它的引用(在其范围内),则可以将 const double ABC 作为简单值处理。如果编译器将其考虑在内,这正是您获得的性能。
【解决方案4】:

您在谈论 C 语言中两个不同的概念:范围和范围。

  • Extent 与变量将持续的时间范围有关。全局变量始终具有全局范围,这意味着它们从程序开始一直持续到程序结束,在整个程序生命周期中从赋值到赋值都保持其值。局部变量通常具有局部范围,这意味着变量在其定义时被创建/分配,并且它的生命周期延伸到包含它的内部块的末尾。通常,编译器根据块的本地存储以及参数的大小和数量(在功能块的情况下),将每个函数/块入口调用的堆栈指针寄存器提前一个固定量,因此添加局部变量只会改变要添加到 SP 寄存器的常量值不会导致额外的执行损失。这也是局部变量在进入时没有被初始化的原因。

  • 范围与变量的可见性有关。每个对象都有三种不同的作用域: 全局作用域 表示变量可以在程序中的任何地方访问; 文件范围表示变量仅在定义它的文件模块内可见,而在其他地方不可见,并且; 局部范围 表示变量从声明点到块结束都是可见的。对于全局范围,您始终可以使用其名称来引用它。相反,局部作用域意味着变量名仅在包含其声明的内部块中可用(即定义对象的内部对 {}

当你使用static这个词时,它意味着两件事,这取决于你在哪里使用它:

  • 如果您在任何块之外使用它,static 表示 文件范围(与 extern 表示全局程序范围相反)默认情况下,变量(和数据)是文件范围和函数(和代码)是全局范围的(对于函数,您必须使用 static 使它们仅在声明文件中可见)。对于文件范围,变量仅在声明它的文件内可见,但它也具有全局范围。在任何块之外(在文件级别)定义的所有对象都具有全局范围(它们从程序开始到程序结束都存在)
  • 如果在块内使用static,则表示该变量具有局部范围但具有全局范围。如果在块内使用extern,则表示对象是这样定义的,但在其他地方(它具有全局范围和范围,我想在本地使用它)

为了完成,extern 的出现并没有让编译器分配一个变量,它只是通知它有一个变量(在别处定义,具有全局范围和范围)具有这种类型和名称并且可以从这里开始,本地访问(或文件范围,如果在任何块之外使用)您必须在某处(在其他文件或此文件中)放置一个声明,而不使用单词 extern 以使编译器为它。对于程序全局变量,您必须在它实际驻留的文件中定义它两次(一个带有 extern 关键字和一个没有它的定义 --- 可能还有一个初始化程序),并在所有其他文件中声明为 extern将使用它。

编辑

对于在程序生命周期内不改变的变量,最好将它们定义为const。这样,您允许编译器记住常量值并使用它的值,而不是在程序中到处引用它。将全局值设为const 有很大的好处。 consts 甚至没有分配内存,如果您从不通过引用使用它们(通过 & 运算符)。

【讨论】:

    【解决方案5】:

    首先,不要去猜测问题是什么(这就是你正在做的事情:)

    其次,让正在运行的程序告诉你什么需要时间。 在您的评论中,您说这需要很多小时。 我使用的方法是在调试器下运行它,中断它,看看它在做什么。 如果您多次这样做,您会发现中断被强烈吸引到问题上。

    使用编译器优化关闭来做到这一点。当你尽可能快地完成它时,然后打开编译器的优化器。 如果你有一些愚蠢的东西需要清理,优化器不会帮你清理掉。这只会让它更难找到。

    This post 展示了我在科学软件中使用它的一些经验。 它还给出了该方法为何如此有效的统计依据。

    我过去发现的大部分时间都花在了这些时间上 a) 使用相同或几乎相同的参数一遍又一遍地调用 explog 之类的函数,b) 调用诸如矩阵乘法或 Cholesky 变换,可以用专门的例程代替。

    【讨论】:

    • 谢谢。我一定会调查的。确实,大部分时间都花在了我的流体求解器的实际计算上,它有很多对 Sinh 和日志的调用。我花了一段时间仔细地预先计算任何出现不止一次的表达式。有了这个特定的部分,我就知道如何尝试加快该部分的速度了。所以现在我正在看剩下的......
    • @Fractal20:我将我感兴趣的问题通过代码生成器运行以创建一个特定于问题的 C 程序,然后我将其编译并链接到我的主程序中。这样就没有时间花在上层建筑上。然后代替对exp 的调用(例如),我使用静态方法来记住参数的先前值和exp 的结果,并调用包装器exp_cached,它基本上记住了函数。这节省了大量时间,因为许多调用的参数很少或从不改变。但同样,首先让样本表明它是必要的。
    【解决方案6】:

    如果我调用一个函数一百万次并且每次调用它 初始化一个临时变量,这是否比我浪费更多时间 有一个它改变的全局温度值吗?

    不一定,因为优化编译器可以将这些变量直接放入寄存器,而不是使用堆栈。 也可以将全局温度值存储在寄存器中,如下所示。在某些架构中,例如具有大量寄存器的 x64,可以提示编译器将常量和其他变量永久分配给某些寄存器。

    足够小的数据可以类似地放在 xmm0..xmm7 上。

    register long int a __asm__("r14");
    
    int foo(int b)
    {
        a = b * 2 + a;
        return a;
    }
    
    int bar(int c)
    {
        a = a * 5 - c;
        return a;
    }
    
    int init(int i) { a = i; }
    

    // in a separate file
    int main(){
      init(3);
      printf("%d %d\n", foo(1), bar(2));
    }
    

    反汇编显示,“全局”变量确实存储在一个寄存器中,在main中只分配了一次

    foo:  leal (%rdi,%rdi), %eax
          cltq
          addq %r14, %rax
          movq %rax, %r14
          ret
    
    bar:  leaq (%r14,%r14,2), %rax
          movslq %edi, %rdi
          subq %rdi, %rax
          movq %rax, %r14
          ret
    
    main:  ...
          movl $3, %r14d
    

    【讨论】:

      猜你喜欢
      • 2020-06-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多