c++ -构造常量数据以提高性能和封装的方法

C++ - what ways to structure constant data for performance and encapsulation?

本文关键字:高性能 封装 方法 常量 数据 c++      更新时间:2023-10-16

很长一段时间以来,我一直使用在constants.h中定义的几个程序范围的常量编写代码:

const size_t kNumUnits = 4;
const float kAlpha = 0.555;
const double kBeta = 1.2345;

这种方法的问题是,在分配固定内存块或循环迭代时,这些信息通常需要在较低的代码级别中,因此这些单元必须#包括这个通用的constants.h,或者在调用这些函数时需要传递相关的值,使接口变得混乱,这些值在运行时永远不会改变,并损害编译器的优化能力。

此外,让所有这些低级代码依赖于几个常量的顶级定义头,对我来说似乎是一种不好的气味。它太像全局变量了,尽管它们都是常量。这使得编写可重用代码变得困难,因为每个需要引用一个或多个常量的组件都必须包含公共头文件。当将一堆组件汇集在一起时,必须手动创建和维护此头。两个或多个组件可以使用相同的常量,但它们都不能自己定义它,因为它们都必须在每个程序中使用相同的值(即使程序之间的值不同),所以它们都需要#include这个高级头文件以及它碰巧提供的所有其他常量——这对封装来说不是很好。这也意味着组件不能"独立"使用,因为它们需要头定义才能工作,但如果它们包含在可重用文件中,那么当组件被引入主项目时,它们需要手动删除。这会导致特定于程序的组件头文件的混乱,每次在新程序中使用组件时都需要手动修改这些头文件,而不是简单地从客户端代码中获取指令。

另一种选择是在运行时通过构造函数或其他成员函数提供相关常量。然而,处理性能对我来说很重要——我有一堆类,它们都对编译时指定的固定大小的数组(缓冲区)进行操作。目前,这个大小要么从constants.h中的常量中获取,要么在运行时作为函数参数传递给对象。我一直在做一些实验,指定数组大小为非类型模板参数const变量,似乎编译器可以产生更快的代码,因为循环大小在编译时是固定的,可以更好地优化。这两个函数是快速的:

const size_t N = 128;  // known at compile time
void foo(float * buffer) {
  for (size_t i = 0; i < N; ++i) {
    buffer *= 0.5f;
  }
}
template <size_t N>  // specified at compile time
void foo(float * buffer) {
  for (size_t i = 0; i < N; ++i) {
    buffer *= 0.5f;
  }
}

与纯运行时版本相反,因为N在编译时不知道,所以不能很好地优化:

void foo(float * buffer, size_t N) {
  for (size_t i = 0; i < N; ++i) {
    buffer *= 0.5f;
  }
}

使用非类型模板参数在编译时传递此信息与#包含全局常量文件及其所有const定义具有相同的性能结果,但它被更好地封装并允许将特定信息(而不是更多)暴露给需要它的组件。

所以我想在声明类型时传递N的值,但这意味着我所有的代码都成为模板化代码(以及将代码移动到.hpp文件中)。似乎只有整型非类型参数是允许的,所以我不能以这种方式传递float或double常量。

template <size_t N, float ALPHA>
void foo(float * buffer) {
  for (size_t i = 0; i < N; ++i) {
    buffer[i] *= ALPHA;
  }
}

所以我的问题是什么是最好的方法来处理这个?人们倾向于如何组织程序范围内的常量以减少耦合,同时仍然获得编译时指定常量的好处?

编辑:

这里有一些使用type来保存常量值的东西,因此可以通过使用模板参数将它们传递到各个层。常量形参在System结构体中定义,并作为System模板形参提供给较低层foobar:

最高层:

#include "foo.h"
struct System {
  typedef size_t buffer_size_t;
  typedef double alpha_t;
  static const buffer_size_t buffer_size = 64;
  //  static const alpha_t alpha = 3.1415;  -- not valid C++?
  static alpha_t alpha() { return 3.1415; } -- use a static function instead, hopefully inlined...
};
int main() {
  float data[System::buffer_size] = { /* some data */ };
  foo<System> f;
  f.process(data);
}

在foo.h中,在中间层:

// no need to #include anything from above
#include "bar.h"
template <typename System>
class foo {
public:
  foo() : alpha_(System::alpha()), bar_() {}
  typename System::alpha_t process(float * data) {
    bar_.process(data);
    return alpha_ * 2.0;
  }
private:
  const typename System::alpha_t alpha_;
  bar<System> bar_;
};

Then in bar.h at 'bottom':

// no need to #include anything 'above'
template <typename System>
class bar {
public:
  static const typename System::buffer_size_t buffer_size = System::buffer_size;
  bar() {}
  void process(float * data) {
    for (typename System::buffer_size_t i = 0; i < System::buffer_size; ++i) {
      data[i] *= static_cast<float>(System::alpha());  -- hopefully inlined?
    }
  }
};  

这确实有一个明显的缺点,那就是把我未来的很多(所有?)代码变成接受"System"参数的模板,以防它们需要引用常数。它也很冗长,难以阅读。但是它确实消除了对头文件的依赖,因为foo和bar不需要预先知道任何关于系统结构的信息。

大量的描述说明了为什么首先需要全局使用这些常量。如果许多模块真的需要这些信息,我想我只是不认为这是一个糟糕的代码气味。这显然是很重要的信息,所以要这样对待它。把它藏起来,把它摊开,或者(哎呀!)把它放在多个地方,似乎是为了改变而改变。添加模板等似乎只是额外的复杂性和开销。

需要考虑的几点:

  • 确保你正在使用预编译的头文件,并且这个common.h在那里
  • 尝试只在实现文件中包含common.h,并且只在真正需要的地方
  • 确保仔细管理这些符号的可见性;不要在每个作用域中都暴露常量,以防万一它们可能需要

另一个想法(受到@Keith上面评论的启发)是你可能想要考虑你的分层。例如,如果您有几个组件,理想情况下每个组件都应该是自包含的,那么您可以拥有这些常量的本地版本,它们恰好是从这些全局常量初始化的。这样可以大大减少耦合并提高局部性/可见性。

例如,主组件可以有自己的接口:
// ProcessWidgets.h
class ProcessWidgets
{
    public:
        static const float kAlphaFactor;
    // ...
};

并在实现中本地化:

// ProcessWidgets.cpp
#include "Common.h"
static const float ProcessWidgets::kAlphaFactor = ::kAlpha;

那么该组件中的所有代码都指向ProcessWidgets::kAlphaFactor