向量重声明与循环操作中的插入-C++

Vector re-declaration versus insertions in looping operations - C++

本文关键字:插入 -C++ 操作 循环 声明 向量      更新时间:2023-10-16

我可以选择在每次调用func()时创建和销毁向量,并在每次迭代中推送元素,如示例a所示。或者修复初始化,在每次迭代时只覆盖旧值,如示例B所示。

示例A:

void func () 
{
std::vector<double> my_vec(5, 0.0);
for ( int i = 0; i < my_vec.size(); i++) {
my_vec.push_back(i);
// do something
}
}
while (condition) {
func();
}

示例B:

void func (std::vector<double>& my_vec) 
{
for ( int i = 0; i < my_vec.size(); i++) {
my_vec[i] = i;
// do something
}
}
while (condition) {
std::vector<double> my_vec(5, 0.0);
func(myVec);
}

这两者中哪一个在计算上比较便宜。数组的大小不会超过10。

我仍然怀疑所问的问题不是预期的问题,但我突然想到,我回答的要点可能不会改变。如果问题更新了,我总是可以编辑这个答案以匹配(或者删除它,如果它不适用的话)。

取消优化的优先级

有多种因素会影响您编写代码的方式。理想的目标包括空间优化、时间优化、数据封装、逻辑封装、可读性、健壮性和正确的功能。理想情况下,所有这些目标都可以在每一段代码中实现,但这并不特别现实。更有可能的情况是,必须牺牲其中一个或多个目标来支持其他目标当这种情况发生时,优化通常应该屈服于其他一切

这并不是说优化应该被忽略。有很多优化很少阻碍更高优先级的目标。这些范围从小到大,比如选择对数算法而不是指数算法,比如通过常量引用而不是通过值。然而,干扰其他目标的优化应该推迟到代码合理完成并正常运行之后。这时,应该使用探查器来确定瓶颈的实际位置。只有当探查器确认优化实现了它们的目标时,这些瓶颈才是其他目标应该屈服于优化的地方。

对于所问的问题,这意味着主要关注的不是计算费用,而是封装。为什么func()的调用者需要为func()分配工作空间?它不应该,除非探查器将其识别为性能瓶颈。如果探查器做到了这一点,那么询问探查器更改是否有帮助要比询问Stack Overflow容易得多(也更可靠!)。

为什么

我可以想到两个主要原因来降低优化的优先级。首先,"嗅觉测试"是不可靠的。虽然可能有少数人可以通过查看代码来识别瓶颈,但还有很多人只是认为自己可以。其次,这就是我们优化编译器的原因。有人想出这种超级聪明的优化技巧,却发现编译器已经在做了,这并非闻所未闻。保持代码干净,让编译器处理例程优化。只有当任务明显超出编译器的能力时才介入。

另请参阅:过早优化

选择优化

好吧,假设探查器确实将这个10元素的小数组的构建确定为瓶颈。下一步是测试替代方案,对吧?几乎首先,你需要一个替代方案,我认为审查各种替代方案的理论益处是有用的。请记住,这是理论性的,剖析者拥有最终发言权。因此,我将从这个问题中探讨替代方案的利弊,以及其他一些可能值得考虑的替代方案。让我们从最糟糕的选择开始,朝着更好的选择努力。

示例A

在示例A中,用5个元素创建一个向量,然后将元素推到向量上,直到i满足或超过向量的大小。看到i和向量的大小在每次迭代中都增加了一个(并且i开始时小于大小),这个循环将运行,直到向量变大到足以使程序崩溃。这意味着可能要进行数十亿次迭代(尽管问题声称规模不会超过10次)。

很容易成为计算成本最高的选项。不要这样做。

示例B

在示例B中,为外部while循环的每次迭代创建一个向量,然后通过引用从func()中访问该向量。这里的性能缺点包括将参数传递给func(),以及让func()通过引用间接访问向量。没有性能优势,因为这可以完成基线(见下文)所要做的一切,再加上一些额外的步骤。

尽管编译器可能能够补偿缺点,但我认为没有理由尝试这种方法。

基线

我使用的基线是对示例a的无限循环的修复。具体而言,将"my_vec.push_back(i);"替换为示例B的"my_vec[i] = i;"。这种简单的方法与我对探查器的初始评估的期望一致。如果你不能打败简单,就坚持下去。

示例B*

该问题的文本对示例B进行了不准确的评估。有趣的是,该评估描述了一种有可能在基线上改进的方法。要获得与文本描述匹配的代码,请将示例B的"std::vector<double> my_vec(5, 0.0);"移到while语句之前的行。这样做的效果是只构造一次向量,而不是每次迭代都构造它。

这种方法的缺点与最初编码的示例B的缺点相同。然而,我们现在获得了一个好处,即向量的构造函数只被调用一次。如果构造比间接成本更昂贵,那么一旦while循环足够频繁地迭代,结果应该是净改进。(注意这些条件:这是一个重要的"如果",并且没有关于多少次迭代"足够"的先验猜测。)尝试一下,看看分析器会说什么是合理的。

获取一些静态

示例B*的一个有助于保持封装的变体是使用基线(固定的示例A),但在向量声明之前使用关键字static。这带来了只构造一次向量的好处,但没有使向量成为参数的相关开销。事实上,由于每次程序执行只进行一次构造,而不是每次启动while循环,因此这种好处可能比示例B*中的好处更大。while循环启动的次数越多,这一好处就越大。

这里的主要缺点是矢量将在整个程序执行过程中占用内存。与示例B*不同,当包含while循环的块结束时,它不会释放内存。在太多地方使用这种方法会导致内存膨胀。因此,尽管介绍这种方法是合理的,但您可能需要考虑其他选项。(当然,如果探查器将其称为瓶颈,使所有其他瓶颈相形见绌,那么成本就足够小了。)

固定大小

我个人的选择是从基线开始,将向量切换到std::array<10,double>。我的主要动机是所需的尺码不会超过10码。同样相关的是CCD_ 21的构造是琐碎的。数组的构造应该与声明double类型的10个变量一样,我认为这是可以忽略的。所以不需要花里胡哨的优化技巧。让编译器来做它自己的事情。

这种方法的预期可能好处是vector在堆上为其存储分配空间,这会带来开销成本。本地array将不具有此成本。然而,这只是一个可能的好处。矢量实现可能已经利用了小矢量的这种性能考虑。(也许在容量需要超过某个神奇的数字,可能超过10之前,它不会使用堆。)我想让你回到我之前提到的"超级聪明"answers"编译器已经在做了"。

我会通过探查器运行这个。如果没有好处,那么其他方法很可能也没有好处。当然,请尝试一下,因为它们已经足够简单了,但最好利用你的时间从其他方面进行优化。