对静态函数变量的访问是否比对全局变量的访问慢?

时间:2018-09-06 07:07:24

标签: c++ static global-variables

Static local variables在第一个函数调用上初始化:

  

在块范围内使用说明符static声明的变量具有静态存储持续时间,但在控件第一次通过其声明时进行初始化(除非其初始化为零或常量初始化,可以在首次进入该块之前执行该初始化) 。在所有其他调用上,将跳过声明。

此外,在C ++ 11中,还有更多检查:

  

如果多个线程尝试同时初始化同一静态局部变量,则初始化仅发生一次(可以使用std :: call_once获得任意函数类似的行为)。   注意:此功能的常规实现使用双重检查的锁定模式的变体,从而将已经初始化的局部静态变量的运行时开销减少到单个非原子布尔比较。 (自C ++ 11起)

与此同时,global variables seem to be initialised on program start(尽管在cppreference上只提到了分配 / 去分配):

  

静态存储持续时间。在程序开始时分配对象的存储空间,在程序结束时释放对象的存储空间。该对象仅存在一个实例。在名称空间范围内声明的所有对象(包括全局名称空间)都具有此存储期限,以及使用静态或外部声明的对象。

因此,给出以下示例:

struct A {
    // complex type...
};
const A& f()
{
    static A local{};
    return local;
}

A global{};
const A& g()
{
    return global;
}

我正确地假设f()每次都要检查其变量是否已初始化,因此f()会比g()慢吗?

4 个答案:

答案 0 :(得分:15)

您在概念上当然是正确的,但是当代建筑可以解决这个问题。

现代的编译器和体系结构将安排管道,从而假定已经初始化的分支。因此,初始化的开销将导致额外的管道转储,仅此而已。

如果有任何疑问,请检查组装。

答案 1 :(得分:6)

是的,几乎可以肯定它会稍微慢一些。但是,在大多数情况下,这并不重要,而且成本会因“逻辑和样式”优势而被抵消。

从技术上讲,函数局部静态变量与全局变量相同。只是它的名称不是全球通用的(em> not (这是一件好事),并且保证它的初始化不仅在确切的指定时间发生,而且仅在线程安全的情况下发生一次。

这意味着局部函数静态变量必须知道初始化是否发生,因此至少需要一个额外的内存访问和一个条件跳转,而全局(原则上)则不需要。 实现可能对全局变量做类似的事情,但不需要(通常不需要)。

有机会在所有情况下(两次除外)正确预测跳跃。前两个调用很有可能被预测为错误(通常默认情况下假定跳转是错误的,而不是默认情况下,第一个调用的假定是错误的,而随后的跳转则假定与最后一个调用的路径相同,同样是错误的)。之后,您应该会很好,接近100%正确的预测。
但是,即使是正确预测的跳转也不是免费的(CPU仍然只能在每个周期启动给定数量的指令,即使假设它们花费零时间完成)也是如此。如果可以成功隐藏可能在最坏情况下可能需要数百个周期的内存延迟,则流水线中的成本几乎就会消失。另外,每个访问都获取一个额外的高速缓存行,否则将不需要该高速缓存行(已初始化的标志可能没有与数据存储在同一高速缓存行中)。因此,您的L1性能会稍差一些(L2应该足够大,这样您就可以说“是的,那又怎样”)。

它还需要真正执行一次全局和原则上不必执行的一次和线程安全操作,至少不会以您看到的方式执行。一个实现可以做一些不同的事情,但是大多数实现只是在输入main之前初始化全局变量,并且很少有大部分是通过memset完成的,或者是隐式的,因为变量存储在始终归零的段中。
您的静态变量必须在执行初始化代码时被初始化,并且必须以线程安全的方式发生。根据您的实现需要花费多少,这可能会非常昂贵。在发现GCC(否则可以正常运行的全能编译器)实际上为每个静态对象锁定了互斥锁之后,我决定放弃线程安全功能,并始终使用fno-threadsafe-statics进行编译(即使这不符合标准)。初始化。

答案 2 :(得分:2)

来自https://en.cppreference.com/w/cpp/language/initialization

  

延迟的动态初始化
  动态初始化是在主函数的第一个语句(对于静态函数)还是在线程的初始函数(对于线程局部变量)之前发生,还是推迟在之后发生,由实现方式定义。

     

如果非内联变量的初始化(自C ++ 17起)推迟在main / thread函数的第一条语句之后发生,那么它会在第一次使用odd使用静态/线程存储持续时间的变量之前发生在与要初始化的变量相同的转换单元中定义。

因此,还必须对全局变量进行类似的检查

因此f()不需要比g() “慢”

答案 3 :(得分:0)

g()不是线程安全的,并且容易受到各种排序问题的影响。安全是要付出代价的。有几种付款方式:

f()(迈耶的单身人士)为每次访问支付费用。如果经常访问或在代码的性能敏感部分中访问过,那么避免使用f()是有意义的。您的处理器可能具有有限数量的电路,可以用于分支预测,无论如何,您都必须在分支之前读取原子变量。仅仅为确保初始化仅发生一次而不断付出代价是不菲的。

如下所述,

h()g()的工作原理非常相似,但具有间接的作用,但是假定h_init()在执行开始时被调用一次。最好,您将定义一个子例程,该子例程被称为main()的行;以绝对顺序调用h_init()之类的每个函数。希望这些对象不需要销毁。

或者,如果您使用GCC,则可以用h_init()注释__attribute__((constructor))。不过,我更喜欢静态init子例程的明确性。

A * h_global = nullptr;
void h_init() { h_global = new A { }; }
A const& h() { return *h_global; }

h2()就像h(),减去额外的间接寻址:

alignas(alignof(A)) char h2_global [sizeof(A)] = { };
void h2_init() { new (std::begin(h2_global)) A { }; }
A const& h2() { return * reinterpret_cast <A const *> (std::cbegin(h2_global)); }