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

时间:2019-01-16 20:37:06

标签: c++ vector time-complexity

我可以选择在每次调用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。

1 个答案:

答案 0 :(得分:1)

我仍然怀疑所要提出的问题不是所要解决的问题,但是我发现答案的重点很可能不会改变。如果问题得到更新,我总是可以编辑该答案以使其匹配(或者,如果结果不适用,则可以将其删除)。

取消优先级优化

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

这并不是说应该忽略优化。有很多优化很少会阻碍较高优先级的目标。这些范围从较小的值(例如通过const引用而不是按值传递)到较大的值(例如选择对数算法而不是指数算法)。但是,确实会干扰其他目标的优化应推迟到代码合理地完成并正常运行之后。此时,应使用探查器确定瓶颈的实际位置。只有在分析器确认优化已实现目标的情况下,这些瓶颈才是其他目标应达到优化的地方。

对于所问的问题,这意味着主要关注的不是计算费用,而是封装。为什么func()的调用者需要分配空间供func()使用?除非分析器将其识别为性能瓶颈,否则不应这样做。而且,如果探查器做到了,那么问探查器是否对更改有所帮助要比问堆栈溢出要容易得多(并且更可靠!)。

为什么?

我可以想到两个主要原因来取消优化的优先级。首先,“嗅探测试”是不可靠的。虽然可能有一些人可以通过查看代码来识别瓶颈,但还有很多人只是认为自己可以做到。其次,这就是为什么我们要优化编译器。有人想到这个超级聪明的优化技巧只是发现编译器已经在做这件事并不是闻所未闻的。保持代码干净,让编译器处理例行优化。仅在任务明显超出编译器的功能时介入。

另请参见:

选择优化

好吧,假设分析器确实将这个由10个元素组成的小数组的构造识别为瓶颈。下一步是测试替代方法,对吗?几乎。首先,您需要一个替代方案,我认为对各种替代方案的理论收益进行回顾是有用的。请记住,这是理论上的,分析器拥有最终决定权。因此,我将探讨该问题中替代方案的优缺点,以及其他一些可能需要考虑的替代方案。让我们从最糟糕的选择开始,逐步走向更好的选择。

示例A

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

轻松实现最昂贵的计算选择。不要这样做。

示例B

在示例B中,为外部while循环的每次迭代创建一个向量,然后通过引用从func()内部对其进行访问。这里的性能缺点包括将参数传递给func(),并使func()通过引用间接访问向量。没有性能专家,因为它可以完成基准(请参见下文)会做的所有事情,外加一些额外的步骤。

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

基线

我使用的基准是对示例A无限循环的一种修复。具体来说,用示例B的“ my_vec.push_back(i);”替换“ my_vec[i] = i;”。这种简单的方法符合我对探查器进行初始评估的期望。如果您无法击败简单的人,那就坚持下去。

示例B *

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

这种方法的缺点与最初编码的示例B的缺点相同。但是,我们现在得到了好处,因为向量的构造函数仅被调用一次。如果构造比间接成本贵,那么一旦while循环足够频繁地迭代,结果应该是净改进。 (请注意这些条件:这是一个很大的“ if”,并且对于“足够”的迭代次数没有先验猜测。)尝试这样做并查看探查器的内容是合理的。

获取一些静态信息

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

这里的主要缺点是向量将在程序执行期间占据内存。与示例B *不同,它在包含while循环的块结束时不会释放其内存。在太多地方使用此方法会导致内存膨胀。因此,尽管对此方法进行分析是合理的,但您可能需要考虑其他选择。 (当然,如果探查器将其称为“瓶颈”,使所有其他方面相形见,,则成本足以支付。)

确定尺寸

我个人尝试选择哪种优化方法是从基线开始并将向量切换为std::array<10,double>。我的主要动机是所需的大小不会超过10。同样重要的是double的构造是微不足道的。数组的构造应与声明10个类型为double的变量保持一致,我希望可以忽略不计。因此,不需要花哨的优化技巧。只需让编译器执行其工作即可。

此方法的预期可能的好处是vector在堆上为其存储分配空间,这会产生开销。本地array不会有此费用。但是,这仅仅是可能的好处。矢量实现可能已经利用了针对小矢量的这种性能考虑。 (也许直到容量需要超过某个魔术数字,甚至可能超过10时,它才会使用堆。)当我提到“ super-clever”和“ compiler已经在做”的时候,我会回头再说。 >

我将通过探查器运行它。如果没有好处,那么其他方法可能就没有好处。可以肯定的,因为它们很简单,请尝试一下,但这可能是更好地利用您的时间来考虑其他方面进行优化的方法。