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它不是一个常量并且它是全局的(=文件范围)并且代码又大又复杂,编译器无法推断sizesize = CalculateSize();. 编译器生成的代码会从变量中获取和重新获取 的值,size并且不能将其“缓存”在寄存器或本地(堆栈上)变量中,该变量可能与其他经常访问的 CPU 的 d-cache 一起位于局部变量。

因此,如果我有类似以下的内容(出于说明目的而编造的示例):

  size = CalculateSize();
  if (size > 200) blah1();
  blah2();
  if (size > 200) blah3();

编译器认为blah1()并且blah2()可能会更改size并生成从sizein读取的内存if (size > 200) blah3();

我想随时随地避免额外的阅读。

显然,像这样的黑客:

const unsigned size = 0;

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

当他们调用未定义的行为时不会这样做。

问题是如何通知编译器它可以“缓存”size曾经size = CalculateSize();执行过的值,并在不调用未定义行为未指定行为以及希望实现特定行为的情况下执行此操作。

这是C++03g++ (4.xx)所需要的。C++11可能是也可能不是一个选项,我不确定,我试图避免使用高级/现代 C++ 功能以保持在编码指南和预定义工具集内。

到目前为止,我只提出了一个技巧,可以size在每个使用它的类中创建一个常量副本并使用该副本,就像这样(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的解决方案以及解决方案中的任何附加代码(尤其是分支和条件分支)执行的频率与size所提到的一样多,也会影响性能。我不想在一个地方赢,却在另一个地方失去同样甚至更多。

这是一个相关的非解决方案,导致 UB(至少在 C 中)。

4

3 回答 3

2

C++ 中有一个register关键字,它告诉编译器您计划大量使用变量。不了解您正在使用的编译器,但大多数现代编译器都会为用户执行此操作,如果需要,将变量添加到注册表中。您还可以将变量声明为常量并使用const_cast.

于 2013-11-10T00:45:50.053 回答
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 上

分支预测器几乎可以完全减轻检查它是否已经初始化的问题。之后你最终会得到一个错误的结果和千万亿个正确的结果。

是的,在管道基本建成后,您仍然必须访问该状态,这可能会对缓存造成严重破坏,但除非您进行分析,否则您无法确定。此外,编译器可能会为你做一些额外的魔法(这正是你要找的),所以我建议你先用这种方法编译和分析,然后再完全放弃它。

于 2013-11-10T00:54:51.163 回答
0

什么:

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

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

GCC 4.8.2

于 2013-11-10T01:03:34.540 回答