15

假设我在一个看起来像这样的单线程程序中有一个函数

void f(some arguments){
    char buffer[32];
    some operations on buffer;
}

并且 f 出现在一些经常被调用的循环中,所以我想让它尽可能快。在我看来,每次调用 f 时都需要分配缓冲区,但如果我将其声明为静态,则不会发生这种情况。这是正确的推理吗?是免费加速吗?仅仅因为这个事实(它很容易加速),优化编译器是否已经为我做了这样的事情?

4

11 回答 11

14

不,这不是免费的加速。

首先,分配开始时几乎是自由的(因为它仅包括向堆栈指针添加 32),其次,至少有两个原因导致静态变量可能更慢

  • 你失去了缓存位置。在堆栈上分配的数据将已经在 CPU 缓存中,因此访问它非常便宜。静态数据分配在不同的内存区域,因此它可能不会被缓存,因此会导致缓存未命中,并且您必须等待数百个时钟周期才能从主内存中获取数据。
  • 你失去了线程安全。如果两个线程同时执行该函数,它将崩溃并烧毁,除非放置一个锁,因此一次只允许一个线程执行该部分代码。这意味着您将失去拥有多个 CPU 内核的好处。

所以这不是免费的加速。但在你的情况下它可能会更快(尽管我对此表示怀疑)。所以尝试一下,对它进行基准测试,看看什么在你的特定场景中最有效。

于 2010-09-16T20:24:13.053 回答
10

在堆栈上增加 32 字节几乎在所有系统上都不会花费任何成本。但是你应该测试一下。对静态版本和本地版本进行基准测试并回发。

于 2010-09-16T19:27:16.677 回答
8

对于将堆栈用于局部变量的实现,分配通常涉及推进寄存器(向其添加值),例如堆栈指针 (SP) 寄存器。这个时序非常微不足道,通常是一条指令或更少。

然而,堆栈变量的初始化需要更长的时间,但同样,并不多。查看您的汇编语言列表(由编译器或调试器生成)以了解详细信息。标准中没有关于初始化变量所需的持续时间或指令数量。

静态局部变量的分配通常被区别对待。一种常见的方法是将这些变量放在与全局变量相同的区域中。通常这个区域的所有变量在调用之前都会被初始化main()。在这种情况下,分配是将地址分配给寄存器或将区域信息存储在内存中的问题。这里没有浪费太多的执行时间。

动态分配是执行周期被烧毁的情况。但这不在你的问题范围内。

于 2010-09-16T19:34:58.220 回答
3

现在的写法,没有分配成本:32 个字节在堆栈上。唯一真正的工作是您需要进行零初始化。

局部静力学在这里不是一个好主意。它不会更快,并且您的函数不能再从多个线程中使用,因为所有调用共享相同的缓冲区。更不用说本地静态初始化不保证是线程安全的。

于 2010-09-16T19:32:11.457 回答
3

我建议解决此问题的更通用方法是,如果您有一个多次调用的函数需要一些局部变量,那么请考虑将其包装在一个类中并使这些变量成为成员函数。考虑是否需要使大小动态化,而不是char buffer[32]使用std::vector<char> buffer(requiredSize). 这比每次通过循环初始化的数组更昂贵

class BufferMunger {
public:
   BufferMunger() {};
   void DoFunction(args);
private:
   char buffer[32];
};

BufferMunger m;
for (int i=0; i<1000; i++) {
   m.DoFunction(arg[i]);  // only one allocation of buffer
}

将缓冲区设为静态还有另一个含义,即该函数现在在多线程应用程序中是不安全的,因为两个线程可能会同时调用它并覆盖缓冲区中的数据。另一方面,BufferMunger在需要它的每个线程中使用单独的线程是安全的。

于 2010-09-16T19:33:03.543 回答
3

请注意,C++(与 C 相对)中的块级static变量在首次使用时被初始化。这意味着您将引入额外运行时检查的成本。该分支最终可能会使性能变得更糟,而不是更好。(但实际上,您应该像其他人提到的那样进行配置。)

无论如何,我认为这不值得,特别是因为你会故意牺牲重入。

于 2010-09-16T19:40:29.860 回答
2

如果您正在为 PC 编写代码,那么无论哪种方式都不太可能有任何有意义的速度优势。在某些嵌入式系统上,避免所有局部变量可能是有利的。在其他一些系统上,局部变量可能更快。

前者的一个例子:在 Z80 上,为具有任何局部变量的函数设置堆栈帧的代码非常长。此外,访问局部变量的代码仅限于使用 (IX+d) 寻址模式,该模式仅适用于 8 位指令。如果 X 和 Y 都是全局/静态变量或都是局部变量,则语句“X=Y”可以组合为:

; 如果两者都是静态的或全局的:6 字节;32 个周期
  ld HL,(_Y) ; 16 个周期
  ld (_X),HL ; 16 个周期
; 如果两者都是本地的:12 字节;56 次循环
  ld E,(IX+_Y) ; 14个周期
  ld D,(IX+_Y+1) ; 14个周期
  ld (IX+_X),D ; 14个周期
  ld (IX+_X+1),E ; 14个周期

除了设置堆栈帧的代码和时间之外,还有 100% 的代码空间损失和 75% 的时间损失!

在 ARM 处理器上,一条指令可以加载位于地址指针 +/-2K 内的变量。如果一个函数的局部变量总计 2K 或更少,则可以用一条指令访问它们。全局变量通常需要两个或更多指令才能加载,具体取决于它们的存储位置。

于 2010-09-16T19:48:24.510 回答
1

使用 gcc,我确实看到了一些加速:

void f() {
    char buffer[4096];
}

int main() {
    int i;
    for (i = 0; i < 100000000; ++i) {
        f();
    }
}

和时间:

$ time ./a.out

real    0m0.453s
user    0m0.450s
sys  0m0.010s

将缓冲区更改为静态:

$ time ./a.out

real    0m0.352s
user    0m0.360s
sys  0m0.000s
于 2010-09-16T19:31:21.003 回答
1

根据变量的具体作用以及它的使用方式,加速几乎为零。因为(在 x86 系统上)堆栈内存是通过一个简单的单个 func(sub esp,amount) 同时为所有本地变量分配的,因此只有一个其他堆栈变量会消除任何增益。唯一的例外是非常大的缓冲区,在这种情况下,编译器可能会坚持 _chkstk 来分配内存(但如果你的缓冲区那么大,你应该重新评估你的代码)。编译器无法通过优化将堆栈内存转换为静态内存,因为它不能假设该函数将在单线程环境中使用,而且它会与对象构造函数和析构函数等混淆

于 2010-09-16T19:32:03.703 回答
1

如果函数中有任何局部自动变量,则需要调整堆栈指针。调整所花费的时间是恒定的,不会根据声明的变量数量而变化。如果您的函数没有任何本地自动变量,您可能会节省一些时间。

如果一个静态变量被初始化,在某处会有一个标志来确定该变量是否已经被初始化。检查标志需要一些时间。在您的示例中,变量未初始化,因此可以忽略这部分。

如果您的函数有可能被递归调用或从两个不同的线程调用,则应避免使用静态变量。

于 2010-09-16T19:35:58.217 回答
1

在大多数实际情况下,它会使函数显着变慢。这是因为静态数据段不在堆栈附近,您将失去缓存一致性,因此当您尝试访问它时会出现缓存未命中。但是,当您在堆栈上分配一个常规 char[32] 时,它就在您所需的所有其他数据旁边,并且访问成本非常低。基于堆栈的 char 数组的初始化成本是没有意义的。

这忽略了静力学还有许多其他问题。

您确实需要实际分析您的代码并查看减速在哪里,因为没有分析器会告诉您分配静态大小的字符缓冲区是一个性能问题。

于 2010-09-16T19:42:19.153 回答