有效地处理 C/C++ 中的函数调用了一百万次

Efficiently treating functions in C/C++ called a million times

本文关键字:函数调用 一百万 处理 C++ 有效地      更新时间:2023-10-16

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

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

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

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

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

不,甚至可能恰恰相反。将在堆栈上创建一个临时变量。

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


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

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

实际上,将变量设置为全局变量甚至会使程序运行速度变慢,因为编译器将不太能够理解和优化代码。

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

但是,如果您的百万次调用是递归调用,那么内存就稀缺,您应该尽最大努力保存它,然后应该采用全局方式。

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

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

常量双倍ABC = 1.234567;

常量对性能很重要。

类型 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);
}

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

你说的是C语言中的两个不同的概念:范围和范围。

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

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

当您使用单词时static,它意味着两件事,具体取决于您使用它的位置:

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

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

编辑

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

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

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

关闭编译器优化的情况下执行此操作。尽可能快地完成它后,然后打开编译器的优化器。 如果你有需要清理的愚蠢的东西,优化器不会为你清理它。这只会使它更难找到。

这篇文章展示了我在科学软件中使用它的一些经验。 它还给出了为什么该方法如此有效的统计理由。

我过去发现的事情是花费了很大一部分时间:a)用相同或几乎相同的参数一遍又一遍地调用explog等函数,b)调用通用函数,如矩阵乘法或cholesky变换,这些函数可能会被专门的例程所取代。

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

不一定,因为优化编译器可以将这些变量直接放入寄存器中,而不是使用堆栈。全局温度值也可以存储在寄存器中,如下所示。在某些体系结构中,例如具有大量寄存器的 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 %dn", 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