优化非成本变量访问

3

我面临一个有趣的优化问题。

在一个由大量类组成的大型代码库中,许多地方经常使用/检查非常量全局(=文件范围)变量的值,并且应该避免对此变量的不必要内存访问。

这个变量只被初始化一次,但由于其初始化的复杂性和需要调用多个函数,因此不能像这样在main()执行之前初始化:

unsigned size = 1000;

int main()
{
  // some code
}

或者

unsigned size = CalculateSize();

int main()
{
  // some code
}

相反,它必须像这样初始化:

unsigned size;

int main()
{
  // some code
  size = CalculateSize();
  // lots of code (statically/dynamically created class objects, whatnot)
  // that makes use of "size"
  return 0;
}

仅仅因为 size 不是一个常量,而是全局变量(文件范围内),且代码又庞大又复杂,编译器无法推断出 size = CalculateSize(); 后面 size 的值不会再次改变。编译器生成的代码需要从变量中获取并重新获取 size 的值,并不能将其“缓存”到寄存器或本地(在堆栈上)变量中,以便与其他经常访问的本地变量一起放在 CPU 的 d-cache 中。
所以,如果我有像下面这样的东西(为说明目的而虚构的示例):
  size = CalculateSize();
  if (size > 200) blah1();
  blah2();
  if (size > 200) blah3();

编译器认为blah1()blah2()可能会更改size,因此在if (size > 200) blah3();中生成了对size的内存读取。

我希望尽可能地避免额外的读取。

显然,像这样的黑客行为:

const unsigned size = 0;

int main()
{
  // some code
  *(unsigned*)&size = CalculateSize();
  // lots more code
}

问题在于如何告诉编译器,一旦执行了size = CalculateSize();,它就可以“缓存”size的值,而不会引发未定义行为未指定行为和希望避免的实现特定行为

这对于C++03g++ (4.x.x)是必需的。 C++11可能是一个选择,也可能不是一个选择,我不确定,我试图避免使用高级/现代C++功能以符合编码准则和预定义工具集。

到目前为止,我只想出了一个hack,即在每个使用它的类中创建一个常量副本,并使用该副本,类似于以下内容(decltype使其成为C++11,但我们可以不用decltype):

#include <iostream>

using namespace std;

volatile unsigned initValue = 255;
unsigned size;

#define CACHE_VAL(name) \
const struct CachedVal ## name \
{ \
  CachedVal ## name() { this->val = ::name; } \
  decltype(::name) val; \
} _CachedVal ## name;

#define CACHED(name) \
  _CachedVal ## name . val

class C
{
public:
  C() { cout << CACHED(size) << endl; }
  CACHE_VAL(size);
};

int main()
{
  size = initValue;
  C c;
  return 0;
}

上述方法只能在一定程度上帮助。是否有更好、更能提示编译器的替代方法是合法的C++?希望找到一种最小干扰(源代码方面)的解决方案。
更新:为了更清楚一点,这是在一个性能敏感的应用程序中。我不是试图凭空消除那个特定变量的不必要读取。我正在尝试让/让编译器生成更优化的代码。任何涉及读/写另一个变量的解决方案,像size一样频繁执行的任何额外代码(特别是分支和条件分支),也会影响性能。我不想在一个地方赢得胜利,只为在另一个地方输掉同样甚至更多的东西。
这里有一个相关的非解决方案,导致UB(至少在C中)。

你尝试过使用 const_cast 吗?看起来这正是它的完美应用场景。 - Vinícius Gobbo A. de Oliveira
@ViníciusGobboA.deOliveira 你具体建议什么? - Alexey Frunze
@AlexeyFrunze 你真的应该尝试使用静态常量变量返回来进行性能分析 - 可以查看我的编辑后的答案。 - ScarletAmaranth
刚刚阅读了 const_cast 的文档,发现它不能用于这个情况。这会导致未定义的行为,也就是说这不是一个选项。 - Vinícius Gobbo A. de Oliveira
3个回答

2

C++中有一个关键字叫做register,它告诉编译器你打算经常使用一个变量。不知道你使用的编译器是什么,但现代大多数编译器都会为用户添加一个变量到注册表中(如果需要的话)。你也可以将变量声明为常量,并使用const_cast进行初始化。


我不认为register在现今有太多意义。而且,在文件范围内无法使用register - Alexey Frunze
不是这样的,这就是为什么我说“现代编译器会为用户完成这个任务”。但我不知道@Alexey Frunze正在使用哪个编译器。我之前不知道文件sope,现在知道了,谢谢! - Paweł Stawarz
我是指@carl正在使用的。不知道为什么它变成了你的名字,而且我无法编辑评论。 - Paweł Stawarz
我并不是很了解g++,也无法在其中找到关于“registry”的信息。但我猜想最好的方法就是添加关键字并进行测试! - Paweł Stawarz

0
#include <iostream>

unsigned calculate() {
    std::cout<<"calculate()\n";
    return 42;
}

const unsigned mySize() {
    std::cout<<"mySize()\n";
    static const unsigned someSize = calculate();
    return someSize;
}

int main() {
    std::cout<<"main()\n";
    mySize();
}

打印:

main()  
mySize()  
calculate()  

在GCC 4.8.0上

检查它是否已经被初始化将几乎完全由分支预测器解决。之后,您将得到一个假的和一千万个真的结果。

是的,在管道基本构建之后,您仍然需要访问该状态,这可能会对缓存造成破坏,但除非您进行分析,否则无法确定。 此外,编译器可能会为您执行一些额外的魔术(这正是您要寻找的),因此我建议您首先使用此方法进行编译和分析,然后再完全放弃它。


0

什么是:

const unsigned getSize( void )
{
  static const unsigned size = calculateSize();
  return size;
}

这会延迟 `size` 的初始化直到第一次调用 `getSize()`,但仍将其保持为常量。

GCC 4.8.2


“calculateSize()”不会在“main()”之前被调用吗? - Alexey Frunze
@AlexeyFrunze 当你第一次调用 getSize() 时,它将被调用。 - Etherealone
好的,但接下来还有另一个问题...为了区分静态变量的状态(已初始化与未初始化),必须编写一些代码从内存中读取另一个变量,对吗? - Alexey Frunze
“必须有一些代码从内存中读取另一个变量,对吧?”我不太明白你的意思。就目前而言,该函数有点像“发射并忘记”。只要您能确保第一次调用它时calculateSize()将正常工作,您实际上不需要知道它是否已被调用...除非您需要 :)。您能否解释得更详细一些? - Carl
显然,静态变量只需要初始化一次。getSize()如何知道是否已经发生了这种初始化?它必须维护状态才能回答这个问题。状态在内存中的某个位置。因此,我们可能不止一次地读取该状态,而不是多次读取size。此外,不仅内存读取计数,还有执行条件分支和任何处理已初始化或未初始化状态的代码。所有这些都会影响性能。 - Alexey Frunze
啊,我明白了,但是我一点都不知道。 - Carl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接