【问题标题】:std::vector::clear() takes more time after code refactoring代码重构后 std::vector::clear() 需要更多时间
【发布时间】:2012-02-23 14:49:13
【问题描述】:

我有一个过程,它用取自另一个数组的值填充一些数组。 它看起来类似于以下代码:

// Point 0
ptrlistVector.clear();

// Point 1
ptrlistVector.resize(50);
const size_t s = ptrlistVector.size();

// Point 2
for (ObjectList::iterator j = objList.begin(); j != objList.end(); ++j)
{
    for (UINT i = 0; i < s; ++i) 
    {
        ptrlistVector[i].push_back(&(*j)); 
    }
}
// Point 3

实际上“push_back”行中有更复杂的代码——我将不同的值推送到一个列表中。这些值取决于某些条件。

声明和定义:

typedef std::list<void*> ObjectPtrList;
typedef std::vector<ObjectPtrList> PtrListVector;
typedef std::list<std::string> ObjectList;

ObjectList objList;
PtrListVector ptrlistVector;

我测量了点之间的时间,平均而言,点 1-0 需要 0.02 秒,点 3-2 需要 0.05 秒。 我试图重构循环并发现了一些奇怪的行为。 我用以下内容替换了上面的循环:

for (UINT i = 0; i < s; ++i)
{
    for (ObjectList::iterator j = objList.begin(); j != objList.end(); ++j)
    {
        ptrlistVector[i].push_back(&(*j)); 
    }
}

在那之后,时间发生了变化。第 3-2 点需要 0.035 秒,但 clear() 调用(第 1-0 点)现在需要 0.45(!!!),这比上一次要大得多。

我使用 MSVC 10.0,Debug 和 Release 模式下的结果大致相同。在Release模式下,时间差不是那么大,但无论如何,第二个的时间会更大。

谁能解释一下为什么在我更改循环后 clear() 调用需要更多时间?

下面的代码是我用于性能测试的控制台应用程序。

#include "stdafx.h"
#include <windows.h>
#include <vector>
#include <list>
#include <cstdio>
#include <cassert>
#include <string>

int _tmain(int argc, _TCHAR* argv[])
{
    typedef std::list<void*> ObjectPtrList;
    typedef std::vector<ObjectPtrList> PtrListVector;
    typedef std::list<std::string> ObjectList;

    ObjectList objList;
    objList.insert(objList.begin(), 500, std::string());

    PtrListVector ptrlistVector;

    LARGE_INTEGER __counters[10];
    double __totals[10] = { 0 };
    UINT __counter = 0;
    BOOL bRes;

    LARGE_INTEGER __freq;
    bRes = QueryPerformanceFrequency(&__freq);
    assert(bRes);

    for (int k = 0; k < 500; ++k)
    {
        // Point 0
        bRes = QueryPerformanceCounter(&__counters[0]);
        ptrlistVector.clear();

        // Point 1
        bRes = QueryPerformanceCounter(&__counters[1]);
        ptrlistVector.resize(50);
        const size_t s = ptrlistVector.size();

        // Point 2
        bRes = QueryPerformanceCounter(&__counters[2]);
        /*
        // original
        for (ObjectList::iterator j = objList.begin(); j != objList.end(); ++j)
        {
            for (UINT i = 0; i < s; ++i) 
            {
                ptrlistVector[i].push_back(&(*j)); 
            }
        }
        /*/
        for (UINT i = 0; i < s; ++i) // refactored
        {
            for (ObjectList::iterator j = objList.begin(); j != objList.end(); ++j)
            {
                ptrlistVector[i].push_back(&(*j)); 
            }
        }
        //*/

        // Point 3  
        bRes = QueryPerformanceCounter(&__counters[3]);
        __counter += 1;
        __totals[1] += 1.0 * (__counters[1].QuadPart - __counters[0].QuadPart) / __freq.QuadPart;
        __totals[2] += 1.0 * (__counters[2].QuadPart - __counters[1].QuadPart) / __freq.QuadPart;
        __totals[3] += 1.0 * (__counters[3].QuadPart - __counters[2].QuadPart) / __freq.QuadPart;
        __totals[4] += 1.0 * (__counters[3].QuadPart - __counters[0].QuadPart) / __freq.QuadPart;
        printf("%s: %.4f  %.4f  %.4f = %.4f\n", 
            __FUNCTION__, 
            __totals[1]/__counter, 
            __totals[2]/__counter, 
            __totals[3]/__counter, 
            __totals[4]/__counter);
    }
    return 0;
}

【问题讨论】:

  • 调用reserve,并在循环之前将元素数量添加到vectorclear 应该会更快。
  • 你确定ptrlistVector.resize(50)?它向您的向量添加了 50 个默认构造的对象(在您的情况下只是空指针),然后您添加了更多项目。有点可疑。
  • 安迪,是的,我确定。我不测量调整大小的调用。而且我不再将项目添加到向量中,而是将项目添加到作为向量元素的空列表中。
  • 抱歉,没抓住重点。我检查了两个版本并收到几乎相同的结果:原始版本为“total 0.0022”,重构版本为“total 0.0023”

标签: c++ visual-c++ optimization loops stl


【解决方案1】:

我想在这个答案的开头加上免责声明 - 这是推测,因为我没有运行问题中的代码,也没有查看所涉及的实际库实现。但我认为这概述了问题中描述的时间上的任何统计上显着差异的可能解释。但是,请记住,此时它猜想。


清除列表向量所需时间的差异可能是由于堆的使用方式以及堆处理列表被销毁时释放的列表元素时可能正在进行的工作.我认为当使用第二种循环类型释放列表元素时,堆中可能会进行更多工作。我只能猜测(我没有单步执行库代码)。

在第一种循环样式中,每个列表每次循环迭代都会添加一个元素;换句话说,循环迭代0 在每个列表中放置一个元素,然后循环迭代1 在每个列表中放置另一个元素,依此类推。

在您的第二个示例中(clear() 操作需要更长的时间),每个列表都是单独建立的;换句话说,ptrlistVector[0] 中的列表被填满,然后ptrlistVector[1] 被填满,依此类推。

我猜想对于第一个循环样式,特定列表中的每个元素与列表中的其他元素连续(在地址空间中)。那是因为在特定列表上的任意两个 push_back() 操作之间的时间里,发生了 50 其他分配以将元素添加到其他列表。

但是,我猜在第二种循环样式中,特定列表中的元素或多或少是连续的,因为这是分配发生的顺序。

现在,让我们考虑一下当列表被销毁时这可能意味着什么(当保存列表的向量被清除时会发生这种情况)。对于元素在地址空间中连续的列表,堆可能会花费大量时间来合并那些相邻的空闲块。但是当一个包含一堆不相邻元素的列表释放它的元素时,释放的内存块是不相邻的,所以不会发生合并。直到我们到达最后一个(或最后几个)列表,堆才可能开始看到可以合并的相邻空闲内存块。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多