是否有一个优雅的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);
}
}
...
}
是 no 解决方案,因为可读性受到的影响更大!
进一步思考,get_text
函数的界面无论如何都是非惯用的,因为昨天的params 所以,而且“好”的界面会按值返回:
// [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);
}
在这里,我们不支付双进行内存分配,因为很有可能通过RVO从返回值构造s
,所以对于[F]我们在[C]中支付与分配开销相同的。 与 [C]案例不同,我们无法优化此接口变体。
所以底线似乎是使用最小范围(可能)会损害性能并使用干净的接口我至少认为值的返回比那些out-ref-param更清晰东西将阻止优化机会 - 至少在一般情况下。
问题并不是说有时人们不得不放弃代码以提高效率,问题是只要Devs开始发现这样的特殊情况,整个编码指南(见[A],[B])失去权威。
问题现在将是:请参阅第一段
答案 0 :(得分:2)
写作显然更有效:[示例D的开始......]
我怀疑这一点。您在循环外部开始支付默认构造。在循环内,get_text
可能会调用重新分配缓冲区(取决于get_text
和string
的定义方式。请注意,对于某些运行,您实际上可能会看到一个改进(例如,在逐渐变短的字符串的情况下)和一些(在每次迭代时字符串长度增加约2倍)的性能受到巨大冲击。
如果不变量构成瓶颈(分析器会告诉你),将不变量从循环中提升是完全合理的。否则,请选择惯用的代码。
答案 1 :(得分:1)
我要么:
for
循环范围内声明一个局部。 std::pair<int,std::string>
将是一种选择,尽管专门的容器可以减少语法噪音。(在许多情况下out参数会比RVO风格快)
答案 2 :(得分:1)
取决于get_text
。
如果你可以实现它,所以它在大多数时间重用字符串对象中分配的空间,然后明确地在循环外声明对象,以避免在每次循环迭代时分配新的动态内存。 / p>
动态分配是昂贵的(最好的单线程分配器需要大约40个指令用于单个分配,多线程增加开销而不是所有分配器都是“最佳”),并且可以分段内存。
(BTW,std::string
通常实现所谓的“小字符串优化”,这避免了对小字符串的动态分配。所以如果你知道你的大多数字符串都足够小,那么{的实现{1}}不会改变,理论上你甚至可以避免动态分配,即使在每次迭代中构造一个新对象时也是如此。但是我会建议不要这样做。)
一般情况下,这完全取决于您使用它们的对象和函数的实现方式。如果您关心性能,则必须根据具体情况处理这些“抽象泄漏”。因此,明智地选择你的战斗:首先测量和优化瓶颈。
答案 3 :(得分:0)
如果你有一个字符串类的copy-on-write实现,那么to_lower会分配内存,所以你不能通过简单地在循环外声明s来获得性能。
在我看来,有两种可能性: 1.)你有一个类,它的构造函数做了一些非常重要的事情,不需要在每次迭代中重新完成。然后,将声明置于循环之外在逻辑上是直截了当的。 2.)你有一个类,其构造函数没有做任何有用的事情,然后将声明放在循环中。
如果1.为真,那么您应该将对象拆分为辅助对象,例如,分配空间并执行非平凡初始化和flyweight对象。如下所示:
StringReservedMemory m (500); /* base object for something complex, allocating 500 bytes of space */
for (...) {
MyOptimizedStringImplementation s (m);
...
}