【问题标题】:std::vector reserve being more costly than expectedstd::vector 储备比预期更昂贵
【发布时间】:2021-01-10 23:01:49
【问题描述】:

因此,为了提高效率,我需要手动处理 std::vector 分配的内存。而且我注意到我的程序比预期的要慢,所以我在我的代码库中到处添加了这个习语:

const uint new_edges_capacity = mesh.edges.size() + 6;
if(new_edges_capacity > mesh.edges.capacity())
    mesh.edges.reserve(new_edges_capacity * 2);

原来我只是在做:

const uint new_edges_capacity = mesh.edges.size() + 6; 
mesh.edges.reserve(new_edges_capacity * 2);

嗯,第一个 sn-p 比第二个快几个数量级。 我不明白,官方文档似乎表明储备应该已经在做同样的检查。但是,perf 绝对将 std::reserve 标记为我的代码中最昂贵的操作,并且确实修改表明不调用 reserve 并依赖它来检查分配更快。

我使用此模式的函数示例:

template<typename V>
void HMesh<V>::SplitFace(uint face_id, HMesh<V>& mesh)
{
    mesh.vertex_data.push_back({});
    mesh.verts.push_back({&mesh});
    HMesh<V>::MVert& c = mesh.verts.back();

    const uint new_edges_capacity = mesh.edges.size() + 6;
    if(new_edges_capacity > mesh.edges.capacity())
        mesh.edges.reserve(new_edges_capacity * 2);
    mesh.edges.push_back({&mesh});
    HMesh<V>::MEdge& n00 = mesh.edges.back();
    mesh.edges.push_back({&mesh});
    HMesh<V>::MEdge& n01 = mesh.edges.back();
    mesh.edges.push_back({&mesh});
    HMesh<V>::MEdge& n10 = mesh.edges.back();
    mesh.edges.push_back({&mesh});
    HMesh<V>::MEdge& n11 = mesh.edges.back();
    mesh.edges.push_back({&mesh});
    HMesh<V>::MEdge& n20 = mesh.edges.back();
    mesh.edges.push_back({&mesh});
    HMesh<V>::MEdge& n21 = mesh.edges.back();

    const uint new_faces_capacity = mesh.faces.size() + 2;
    if(new_faces_capacity > mesh.faces.capacity())
        mesh.faces.reserve(new_faces_capacity * 2);
    mesh.faces.push_back({&mesh});
    HMesh<V>::MFace& f1 = mesh.faces.back();
    mesh.faces.push_back({&mesh});
    HMesh<V>::MFace& f2 = mesh.faces.back();

    auto& face = mesh.faces[face_id];

    HMesh<V>::MFace& f0 = face;

    HMesh<V>::MEdge& e0 = face.EdgeD();
    HMesh<V>::MEdge& e1 = face.EdgeD().NextD();
    HMesh<V>::MEdge& e2 = face.EdgeD().PrevD();

    HMesh<V>::MVert& v0 = e0.VertD();
    HMesh<V>::MVert& v1 = e1.VertD();
    HMesh<V>::MVert& v2 = e2.VertD();

    ConnectFace(f0, n00, e0, n10);
    ConnectFace(f1, n11, e1, n21);
    ConnectFace(f2, n20, e2, n01);

    Pair(n00, n01);
    Pair(n10, n11);
    Pair(n20, n21);

    AttachVertices(n00, c, v0);
    AttachVertices(n11, c, v1);
    AttachVertices(n20, c, v2);

    c.Data({
        (v0.Data().position + v1.Data().position + v2.Data().position) / 3.0,
        (v0.Data().uv + v1.Data().uv + v2.Data().uv) / 3.0,
        {0,0,1}
    });
}

【问题讨论】:

  • 没有优化的 Gcc(由于调试原因)
  • 第一个装满后会加倍。您的第二个可能会在每次运行时添加 2 个元素。你的第二个做了更多的保留。
  • reserve 检查是否为new_edges_capacity * 2 &gt; capacity(),但您正在检查new_edges_capacity &gt; capacity()。没有优化的时间也是没有意义的
  • 对数千个数据点进行线性运算的时序并非毫无意义。以防万一,此更改使代码数量级更快。
  • 但我也认为你的评论是我的问题。

标签: c++ memory vector stl


【解决方案1】:

您不是在重新实现reserve,而是在容量耗尽时重新实现调整大小操作。问题是,如果您每次插入项目时都这样做,那么您每次都在调整大小,使 every 操作O(n)(因为它必须将所有项目从原始后备存储移动到新的,更大的,存储)。如果你每次都运行这个,从一个空的 vector 构建,你会看到以下模式:

Size  Capacity  Reallocation needed?
1     14        Yes
2     16        Yes (16 > 14)
3     18        Yes (18 > 16)
4     20        Yes (20 > 18)
... reallocations continue forever ...

并且每次调整大小都会导致新的分配、所有现有元素的移动以及旧分配的清理(触发移动对象的析构函数)。您还没有保存任何东西,因为您每次都强制进行新分配,以避免不必要的分配(这显然没有帮助)。

if 测试的情况下,由于如果测试失败,您测试的值是reserve 的一半,因此它重新分配的频率要低得多:

Size  Capacity  Reallocation needed?
1     14        Yes
2     14        No  (2 + 6 <= 14)
3     14        No  (3 + 6 <= 14)
4     14        No  (4 + 6 <= 14)
5     14        No  (5 + 6 <= 14)
6     14        No  (6 + 6 <= 14)
7     14        No  (7 + 6 <= 14)
8     14        No  (8 + 6 <= 14)
9     30        Yes  (9 + 6 > 14)
... very rare reallocations ...

如您所见,您的测试意味着更少的调整大小活动。

如果您只是想猜测您需要多少空间,请不要;让vector 按需自动调整大小(每次必要时加倍可能会比默认的摊销增长计划节省一点时间,但也会浪费大量内存)。 reserve 是在您知道需要多少空间时使用的;一遍又一遍地使用它很昂贵。

【讨论】:

  • 但是文档说:`如果 n 大于当前向量容量,该函数会导致容器重新分配其存储,将其容量增加到 n(或更大)。在所有其他情况下,函数调用不会导致重新分配,并且向量容量不受影响。`它表示如果容量不超过限制,则不应进行分配
  • @Makogan:容器的大小是否不断变化?如果没有minimal reproducible example,我的假设是您在插入vector 之前或之后运行它,这意味着新请求的容量总是比上次请求的容量大一点。
  • @Makogan:请记住,您的测试是询问新容量是否大于当前容量,但它会将大小调整为该新容量的 两倍。文档说“如果您要求的容量完全大于当前容量,我们必须调整大小”,但您的测试确保您只在容量太多时才要求更大的容量更大,显着减少您需要调整后备存储大小的次数。
  • @Makogan 并查看答案图表中的“容量”。每次你告诉它要扩大容量,所以每次它都必须调整大小。 16 大于 14。18 大于 16。
  • 是的,在所有情况下,我的预订金额都是要求的两倍。
【解决方案2】:

您根本不需要预订。只需使用索引。

template<typename V>
void HMesh<V>::SplitFace(uint face_id, HMesh<V>& mesh)
{
    mesh.vertex_data.push_back({});
    mesh.verts.push_back({&mesh});
    HMesh<V>::MVert& c = mesh.verts.back();

    const uint n = mesh.edges.size();
    mesh.edges.push_back({&mesh});
    mesh.edges.push_back({&mesh});
    mesh.edges.push_back({&mesh});
    mesh.edges.push_back({&mesh});
    mesh.edges.push_back({&mesh});
    mesh.edges.push_back({&mesh});
 
    auto & n00 = mesh.edges[n + 0];
    auto & n01 = mesh.edges[n + 1];
    auto & n10 = mesh.edges[n + 2];
    auto & n11 = mesh.edges[n + 3];
    auto & n20 = mesh.edges[n + 4];
    auto & n21 = mesh.edges[n + 5];

    const uint f = mesh.faces.size();
    mesh.faces.push_back({&mesh});
    mesh.faces.push_back({&mesh});

    auto& f0 = mesh.faces[face_id];
    auto& f1 = mesh.faces[f + 0];
    auto& f2 = mesh.faces[f + 1];

    auto& e0 = f0.EdgeD();
    auto& e1 = f0.EdgeD().NextD();
    auto& e2 = f0.EdgeD().PrevD();

    auto& v0 = e0.VertD();
    auto& v1 = e1.VertD();
    auto& v2 = e2.VertD();

    ConnectFace(f0, n00, e0, n10);
    ConnectFace(f1, n11, e1, n21);
    ConnectFace(f2, n20, e2, n01);

    Pair(n00, n01);
    Pair(n10, n11);
    Pair(n20, n21);

    AttachVertices(n00, c, v0);
    AttachVertices(n11, c, v1);
    AttachVertices(n20, c, v2);

    c.Data({
        (v0.Data().position + v1.Data().position + v2.Data().position) / 3.0,
        (v0.Data().uv + v1.Data().uv + v2.Data().uv) / 3.0,
        {0,0,1}
    });
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-03-14
    • 2010-10-12
    • 2021-12-20
    • 2013-03-22
    • 2011-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多