繁体   English   中英

访问静态函数变量比访问全局变量要慢吗?

[英]Is access to a static function variable slower than access to a global variable?

静态局部变量在第一个函数调用时初始化:

在块作用域中使用指定符static声明的变量具有静态存储持续时间,但是在控件第一次通过其声明时初始化(除非它们的初始化为零或初始化初始化,这可以在首次输入块之前执行)。 在所有进一步的调用中,将跳过声明。

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

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

同时, 全局变量似乎在程序启动时初始化 (尽管技术上只在cppreference上提到了分配 / 解除分配 ):

静态存储时间。 程序开始时分配对象的存储空间,程序结束时分配存储空间。 只存在一个对象实例。 在命名空间范围(包括全局命名空间)声明的所有对象都具有此存储持续时间,以及使用static或extern声明的持续时间

所以给出以下示例:

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

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

我是否正确假设f()必须检查每次调用它的变量是否被初始化,因此f()将比g()慢?

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

现代编译器和体系结构将安排管道,以便假定已经初始化的分支。 因此,初始化的开销会产生额外的管道转储,这就是全部。

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

是的,它几乎肯定会稍微慢一些。 然而,大部分时间它都无关紧要,成本将超过“逻辑和风格”的好处。

从技术上讲,函数本地静态变量与全局变量相同。 只是它的名称不是全局已知的(这是一件好事),并且它的初始化保证不仅发生在确切的指定时间,而且只发生一次,并且线程安全。

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

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

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

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

延迟动态初始化
它是实现定义的,是否在主函数的第一个语句(用于静态)或线程的初始函数(用于线程本地)之前发生动态初始化,或者延迟发生在之后。

如果非内联变量(因为C ++ 17)的初始化延迟发生在主/线程函数的第一个语句之后,它发生在任何变量的第一次使用之前,其中静态/线程存储持续时间定义在与要初始化的变量相同的翻译单元。

因此,对于全局变量也可能需要进行类似的检查。

所以f()不必比g() “慢”

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

f() ,Meyer的Singleton,为每次访问付出代价。 如果频繁访问或在代码的性能敏感部分访问,那么避免使用f()确实有意义。 您的处理器可能具有可用于分支预测的有限数量的电路,并且您无论如何都被迫在分支之前读取原子变量。 仅仅确保初始化只发生一次,这是一个很高的代价。

h() ,如下所述,与g()非常相似,具有额外的间接性,但假设h_init()在执行开始时只被调用一次。 最好你定义一个被调用为main()行的子程序; 它使用绝对排序调用像h_init()这样的每个函数。 希望这些物体不需要被破坏。

或者,如果使用GCC,则可以使用__attribute__((constructor))注释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)); }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM