16

问题第一

C++ 中是否有一种优雅的解决方案来防止必须声明复杂的对象变量,这些变量只在循环外的循环中使用,以提高效率?

详细解释

一位同事提出了一个有趣的观点。到我们的代码策略,它声明(解释):始终使用最小范围的变量并在第一次初始化时声明变量

编码指南示例:

// [A] DO THIS
void f() {
  ...
  for (int i=0; i!=n; ++i) {
    const double x = calculate_x(i);
    set_squares(i, x*x);
  }
  ...
}

// [B] DON'T do this:
void f() {
  int i;
  int n;
  double x;
  ...
  for (i=0; i!=n; ++i) {
    x = calculate_x(i);
    set_squares(i, x*x);
  }
  ...
}

这一切都很好,这当然没有错,直到你从原始类型转移到对象。(对于某种界面

例子:

// [C]
void fs() {
  ...
  for (int i=0; i!=n; ++i) {
    string s;
    get_text(i, s); // void get_text(int, string&);
    to_lower(s);
    set_lower_text(i, s);
  }
  ...
}

在这里,字符串 s 将被破坏,每个循环周期都会释放内存,然后每个循环get_text函数都必须为 s 缓冲区重新分配内存。

写起来显然更有效:

  // [D]
  string s;
  for (int i=0; i!=n; ++i) {
    get_text(i, s); // void get_text(int, string&);
    to_lower(s);
    set_lower_text(i, s);
  }

因为现在 s 缓冲区中分配的内存将在循环运行之间保留,我们很可能会节省分配。

免责声明: 请注意:由于这是循环并且我们正在谈论内存分配,因此我认为一般考虑这个问题是过早的优化。当然,有些情况和循环的开销并不重要。但是n有一个比开发人员最初期望的更大的唠叨倾向,并且代码有在性能确实很重要的环境中运行的唠叨倾向。

无论如何,现在“通用”循环构造的更有效方法是违反代码局部性并声明复杂对象不合适,“以防万一”。这让我相当不安。

请注意,我考虑这样写:

// [E]
void fs() {
  ...
  {
    string s;
    for (int i=0; i!=n; ++i) {
      get_text(i, s); // void get_text(int, string&);
      to_lower(s);
      set_lower_text(i, s);
    }
  }
  ...
}

没有解决方案,因为可读性会受到更大的影响!

进一步思考get_text函数的接口无论如何都是非惯用的,因为输出参数无论如何都是昨天的,一个“好”的接口会按值返回:

  // [F]
  for (int i=0; i!=n; ++i) {
    string s = get_text(i); // string get_text(int);
    to_lower(s);
    set_lower_text(i, s);
  }

在这里,我们不会为内存分配支付双倍的费用,因为极有可能s会通过 RVO 从返回值构造,所以对于 [F],我们支付与[C] 相同的分配开销。然而,与[C] 的情况不同,我们无法优化此接口变体。

因此,底线似乎是使用最小范围(可能)会损害性能并使用干净的接口我至少认为按值返回比 out-ref-param 的东西会阻止优化机会要干净得多——至少在一般情况下.

问题不在于有时为了效率而不得不放弃干净的代码,问题在于一旦开发人员开始发现这种特殊情况,整个编码指南(参见 [A]、[B])就会失去权威.

现在的问题是:见第一段

4

4 回答 4

2

写起来显然更有效:[示例 D 的开头 ...]

我怀疑这一点。您要为从循环外开始的默认构造付费。在循环中,有可能get_text调用重新分配缓冲区(取决于您get_textstring定义方式)。请注意,对于某些运行,您实际上可能会看到改进(例如,在您获得逐渐变短的字符串的情况下),而对于某些(每次迭代时字符串长度增加约 2 倍),性能会受到巨大影响。

如果不变量构成瓶颈(分析器会告诉您),将不变量从循环中提升出来是非常有意义的。否则,请使用惯用的代码。

于 2012-05-26T20:11:27.067 回答
1

我要么:

  • 为这些重量级人物破例。像“D”,请注意您可以根据需要限制范围。
  • 允许辅助函数(字符串也可以是参数)
  • 如果你真的不喜欢这些,你可以在你的for循环范围内声明一个本地,使用一个包含你的计数器/迭代器和临时的多元素对象。std::pair<int,std::string>将是一种选择,尽管专门的容器可以减少句法噪音。

(在许多情况下,out 参数会比 RVO 样式更快)

于 2012-05-26T20:09:50.733 回答
1

取决于get_text.

如果您可以实现它以便它在大多数情况下重用字符串对象中分配的空间,那么一定要在循环之外声明该对象以避免在每次循环迭代时进行新的动态内存分配。

动态分配是昂贵的(最好的单线程分配器一次分配需要大约 40 条指令,多线程会增加开销,并且并非所有分配器都是“最佳”的),并且会导致内存碎片化。

(顺便说一句,std::string通常实现所谓的“小字符串优化”,它避免了小字符串的动态分配。所以如果你知道你的大部分字符串足够小,并且实现std::string不会改变,理论上你甚至可以避免动态分配在每次迭代中构造一个新对象时。但是这将非常脆弱,所以我建议不要这样做。)


在一般情况下,这完全取决于您的对象和使用它们的函数是如何实现的。如果您关心性能,则必须根据具体情况处理这些“抽象泄漏”。所以,明智地选择你的战斗:首先测量和优化瓶颈。

于 2012-05-26T20:48:32.327 回答
0

如果你有一个字符串类的写时复制实现,那么 to_lower(s) 无论如何都会分配内存,所以不清楚你是否可以通过简单地在循环外声明 s 来获得性能。

在我看来,有两种可能性: 1.) 你有一个类,它的构造函数做了一些不平凡的事情,不需要在每次迭代中重新完成。然后将声明放在循环之外在逻辑上很简单。2.) 你有一个类,它的构造函数没有做任何有用的事情,然后将声明放在循环中。

如果 1. 为真,那么您可能应该将您的对象拆分为一个辅助对象,例如,分配空间并进行非平凡的初始化,以及一个享元对象。类似于以下内容:

StringReservedMemory m (500); /* base object for something complex, allocating 500 bytes of space */
for (...) {
   MyOptimizedStringImplementation s (m);
   ...
}
于 2012-05-26T20:46:55.857 回答