哪个更好:保留向量容量、预分配大小或在循环中推回?

What is better: reserve vector capacity, preallocate to size or push back in loop?

我有一个函数将指向 char 数组和段大小的指针作为输入参数,并调用另一个需要 std::array<std::string> 的函数。思路是将输入的char数组"sectioned"等分,组成string数组

输入字符数组格式是几个确定大小的较小数组(或字符串)连接在一起。这些不是假定的零终止,尽管它们可能是。段大小 5 和元素数 10 的示例:

char k[] = "1234[=10=]01234[=10=]01234[=10=]01234[=10=]01234[=10=]01234[=10=]01234[=10=]01234[=10=]01234[=10=]01234[=10=]0";
char m[] = "1234[=10=]067890987654321[=10=]0234567809876[=10=]05432[=10=]0[=10=]0[=10=]03456789098";
char n[] = "12345678909876543211234567890987654321123456789098";

所有char数组的长度为51(段*元素+1)。我的目标是使函数有效地使用资源,最重要的是执行时间。

因为给猫剥皮的方法有很多种,我有两种(或三种)方法来解决这个问题,问题是,哪种是"better"?我的意思是更快,更少的资源浪费。我不是专业人士,请耐心等待。

这里,values是预分配的,然后每个字符串赋值。

void myfnc_1(void *a_src, uint32_t a_segment) {
    // a_segment = 5 for example
    size_t nSize = GetSize(); // another method, gets 10
    std::vector<std::string> values(nSize);
    char* v = a_src; // take k, n or m for example

    for (size_t i = 0; i < nSize; ++i) {
        values.at(i).assign(v, a_segment);
        v += a_segment;
    }
}

这里没有分配向量,但是每次迭代都会添加一个新的字符串。

void myfnc_1(void *a_src, uint32_t a_segment) {
    size_t nSize = GetSize();
    std::vector<std::string> values();
    char* v = a_src;

    for (size_t i = 0; i < nSize; ++i) {
        values.push_back("");
        values.back().assign(v, a_segment);
        v += a_segment;
    }
}

可能还有第三种方法,那更好。我对矢量不是很有经验,所以我不太清楚。如果段长度和元素数量通常较大 (5, 10) 或较小 (100, 10000),它们是否会有所不同?

第一个post,大粉丝:)

做任何更容易阅读和维护的事情。通常,它是最快的解决方案。

即使它不是最快的,谁在乎呢?也许您的应用会慢 1%。

避免动态重新分配会达到更好的性能,因此请尝试让向量内存足够大以容纳所有元素。

您的第一个解决方案会更有效,因为如果 nSize 大于默认向量容量,则第二个解决方案将需要重新分配才能存储所有元素。

正如 Melkon 所说,reserve 更好:

void myfnc_1(void *a_src, uint32_t a_segment) {
    size_t nSize = GetSize();
    std::vector<std::string> values;
    values.reserve( nSize );
    char* v = a_src;

    for (size_t i = 0; i < nSize; ++i) {
        values.push_back( std::string( v, a_segment ) );
        v += a_segment;
    }
}

Do not use parentheses to invoke the default constructor.

push_back 每次超出容量时都需要额外的重新分配。因此,可以通过保留足够的 space 来避免重新分配来改进选项 2。直接压入字符串也比压入一个空字符串然后再重新分配更有效。 std::string 有一个构造函数,非常适合您的需求:from sequence (5) string (const char* s, size_t n);

关于选项 1:预分配整个向量需要构造每个元素一次以进行初始化,再构造一次以进行赋值。最好保留而不构建元素,直接 push_back 你真正想要的。

这是使用这些改进的代码:

void myfnc_1(void *a_src, uint32_t a_segment)
{
    std::vector<std::string> values;
    size_t nSize = GetSize( );
    values.reserve(nSize);
    char* v = static_cast<char*> ( a_src );

    for (size_t i = 0; i < nSize; ++i)
    {
        values.push_back( std::string( v, a_segment) );
        v += a_segment;
    }
}

向向量添加元素

有几种方法可以将数据添加到向量中:

  • 创建一个空向量,push_back() 个元素放入其中。
  • 创建一个空向量,用 reserve() 分配一些容量,然后将 push_back() 个元素放入其中。
  • 创建一个包含 n 个元素的向量,使用索引和复制赋值。
  • 创建一个空向量,emplace_back() 个元素放入其中。
  • 创建一个空向量,用 reserve() 分配一些容量,然后将 emplace_back() 个元素放入其中。

还有其他方法,例如使用一对迭代器创建容器,或通过标准库算法填充它。我不会在这里考虑这些。

一般注意事项

以下两个注意事项对于以下分析很重要:

  • 避免(重新)分配: 内存分配很慢。重新分配通常涉及将容器中已有的所有内容复制到新位置。
  • 避免不必要的工作:构造你想要的元素比默认构造然后赋值更好。最好在你想要的地方构造一个元素,而不是在其他地方构造它,然后复制。

其他因素也会影响所选解决方案的效率,但这些都是我们可以直接控制的重要因素。通过分析您的代码,其他因素可能会变得明显。

push_back()

每个 push_back() 从参数到 push_back() 调用复制构建向量中的一个元素。如果向量 size() == capacity(),将执行重新分配。这通常(但可能不总是)使容量翻倍,并可能导致将 所有 现有元素复制到新存储中。

push_back() 预分配

使用reserve() 在我们开始之前为元素分配足够的内存。如果您知道(或有一个合理的猜测)元素的数量,那么这样做总是值得的。如果你在猜测,高估总比低估好。

push_back() 调用仍将使用元素类型的复制构造函数,但不应该有任何分配,因为已经提供了 space。您只需在 reserve() 调用期间支付单次分配的费用。如果您对现有容量执行 运行,push_back() 将重新分配,通常会使容量翻倍。这就是为什么高估尺寸更好;您不太可能获得重新分配,而如果低估,您不仅可能会重新分配,而且您将浪费内存分配几乎是您需要的两倍!

请注意,"doubling capacity" 行为未由标准指定,但它是一种常见的实现,旨在减少使用 push_back() 未知大小的数据集时的重新分配频率。

索引和元素赋值

在这里,我们创建了一个由正确数量的默认构造元素组成的向量,然后使用复制赋值运算符将它们替换为我们想要的元素。这只有一个分配,但如果复制分配做任何复杂的事情可能会很慢。这对于未知(或仅猜测)大小的数据集并不适用;仅当您知道索引永远不会超过 size() 时,元素索引才是安全的,如果您需要更多,则必须求助于 push_back() 或调整大小。这不是一个好的通用解决方案,但有时它可以工作。

emplace_back()

emplace_back() 使用 emplace_back() 调用的参数就地构造元素。这通常比等效的 push_back() 更快(但并非总是如此)。它仍然以与 push_back() 相同的模式分配,保留一些容量,填充它,然后在需要更多时重新分配。许多相同的论点都适用,但您可以从构造方法中获得一些收益。

emplace_back() 预分配

这应该是 C++11 或更高版本代码库的首选策略。您获得 emplace_back() 效率(如果可能)并避免重复分配。在列出的机制中,这在大多数情况下预计是最快的。

关于效率的说明

效率可以通过多种方式衡量。执行时间是一种常见的衡量标准,但并不总是最相关的。关于使用哪种策略的一般建议是基于经验和本质上 "averages" 各种效果,以提供一些关于首先做什么的合理陈述。与往常一样,如果任何类型的效率对您的应用程序至关重要,那么确保优化正确位置的唯一方法是 分析 您的代码,进行更改,然后 profile 再次证明更改已达到预期效果。不同的数据类型、硬件、I/O 要求等都会影响这种时序,在您 profile 之前,您永远不会知道这些影响如何结合到您的特定应用程序中.

例子

实例:http://coliru.stacked-crooked.com/a/83d23c2d0dcee2ff

在这个例子中,我使用上面列出的每种方法用 10,000 个字符串填充一个向量。我对每一个计时并打印结果。

这与您的问题类似,但不完全相同;你的结果会有所不同!

注意 emplace_back()reserve() 是最快的,但是这里的索引和赋值也很快。这可能是因为 std::string 有一个高效的 swap(),并且它的默认构造函数没有做太多事情。其他方法要慢一个数量级。

#include <chrono>
#include <iostream>
#include <vector>

using Clock = std::chrono::high_resolution_clock;
using time_point = std::chrono::time_point<Clock>;

std::vector<std::string> strings = {"one", "two", "three", "four", "five"};

std::chrono::duration<double> vector_push_back(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    for (size_t i = 0; i < n; ++i) {
        v.push_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_push_back_with_reserve(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    v.reserve(n);
    for (size_t i = 0; i < n; ++i) {
        v.push_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_element_assignment(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v(n);
    for (size_t i = 0; i < n; ++i) {
        v[i] = strings[i % strings.size()];
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_emplace_back(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    for (size_t i = 0; i < n; ++i) {
        v.emplace_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

std::chrono::duration<double> vector_emplace_back_with_reserve(const size_t n) {
    time_point start, end;
    start = Clock::now();

    std::vector<std::string> v;
    v.reserve(n);
    for (size_t i = 0; i < n; ++i) {
        v.emplace_back(strings[i % strings.size()]);
    }

    end = Clock::now();
    return end - start;
}

int main() {
    const size_t n = 10000;
    std::cout << "vector push_back: " << vector_push_back(n).count() << "\n";
    std::cout << "vector push_back with reserve: " << vector_push_back(n).count() << "\n";
    std::cout << "vector element assignment: " << vector_element_assignment(n).count() << "\n";
    std::cout << "vector emplace_back: " << vector_emplace_back(n).count() << "\n";
    std::cout << "vector emplace_back with reserve: " << vector_emplace_back_with_reserve(n).count() << "\n";
}

结果:

vector push_back: 0.00205563
vector push_back with reserve: 0.00152464
vector element assignment: 0.000610934
vector emplace_back: 0.00125141
vector emplace_back with reserve: 0.000545451

结论

对于大多数新代码,使用 reserve()emplace_back()(或对旧代码使用 push_back())应该会给您一个很好的效率近似值。如果还不够,分析它并找出瓶颈在哪里。它可能不会像你想象的那样。