【问题标题】:Optimize non-cost variable access优化非成本变量访问
【发布时间】:2013-11-21 23:40:50
【问题描述】:

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

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

这个变量被初始化了一次,但是由于它的初始化比较复杂,需要调用很多函数,所以不能这样初始化,在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 并在if (size > 200) blah3(); 中生成从size 读取的内存。

我希望尽可能避免额外阅读。

显然,像这样的黑客攻击:

const unsigned size = 0;

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

不会这样做,因为它们会调用未定义的行为。

问题是如何通知编译器它可以“缓存”size 的值一旦执行了size = CalculateSize(); 并且不调用未定义的行为未指定的行为,希望实现特定的行为

C++03g++ (4.x.x) 需要此功能。 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 那样经常执行的解决方案中的任何附加代码(尤其是分支和条件分支)也会影响性能。我不想在一个地方赢,却在另一个地方失去同样甚至更多。

这是一个相关的non-solution, causing UB(至少在 C 中)。

【问题讨论】:

  • 你试过const_cast吗?看起来这是一个完美的用例。
  • @ViníciusGobboA.deOliveira 你到底在暗示什么?
  • @AlexeyFrunze 您真的应该尝试使用静态 const 变量方法的返回来分析 - 检查我编辑的答案。
  • 只看const_cast的文档,不能用于这个。它将导致未定义的行为,这意味着它不是一个选项。

标签: c++ optimization constructor constants


【解决方案1】:

C++ 中有 register 关键字,它告诉编译器您计划大量使用变量。不知道您正在使用的编译器,但大多数现代编译器都会为用户执行此操作,并在需要时将变量添加到注册表中。您还可以将变量声明为常量并使用const_cast 对其进行初始化。

【讨论】:

  • 我不认为register 这些天很有意义。此外,您不能在文件范围内使用register
  • 不是,这就是为什么我说“现代编译器为用户这样做”。但我不知道@Alexey Frunze 正在使用的编译器。我不知道文件sope,很高兴知道!
  • 我的意思是@carl 正在使用。不知道为什么它变成了你的名字,我无法编辑评论。
  • 我不太了解 g++,也无法在其中找到有关 registry 的信息。但我想找出答案的最好方法就是添加关键字并测试!
【解决方案2】:

什么:

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

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

GCC 4.8.2

【讨论】:

  • calculateSize() 不会在main() 之前被调用吗?
  • @AlexeyFrunze 第一次调用getSize()时会调用
  • 好的,但是还有一个问题……为了区分那个静态变量的状态(初始化与未初始化),必须有一些代码从内存中读取另一个变量,对吧?
  • "必须有一些代码从内存中读取另一个变量,对吧?"我不太明白你的意思。事实上,该功能有点像一劳永逸。只要您可以确保第一次调用它时,calculateSize() 将起作用,您实际上并不需要知道它是否已被调用...除非您这样做:)。你能解释更多吗?
  • 显然,静态变量只能初始化一次。 getSize() 如何知道这个初始化是否已经发生?它必须保持状态才能回答这个问题。状态在某个地方的内存中。因此,我们可能最终会多次阅读该状态,而不是多次阅读size。此外,不仅是内存读取计数,还包括执行条件分支的代码以及处理该初始化或未初始化状态的任何代码。所有这些都会影响性能。
【解决方案3】:
#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 上

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-12-12
    • 1970-01-01
    • 2021-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多