【问题标题】:C++: How to deal with the RAM consumed by unused empty vectors?C++:如何处理未使用的空向量消耗的 RAM?
【发布时间】:2019-05-21 01:19:47
【问题描述】:

我有一个执行基于代理的模拟的 C++ 软件。在模拟中,有Populations 包含Patches,其中包含Individuals,其中包含两个Haplotypes。每个Haplotype 包含 12 个向量,每个向量用于跟踪不同类型的基因。

class Haplotype
{
    std::vector<A> genesA;
    std::vector<B> genesB;
    std::vector<C> genesC;
    std::vector<D> genesD;
    ....

};

然而,在实践中,用户永远不会使用超过一种或两种类型的基因。这意味着这些未使用的向量中的每一个都消耗几个字节(至少两个指针)。对于具有大量基因和少量Individuals 的模拟,可以忽略不计。然而,对于很少基因和大量个体的模拟,这些额外的字节可能开始变得重要。性能(CPU 时间和 RAM)是最重要的。

是否有一个很好的设计模式可以让我处理这个问题?可以仅按需将向量添加到Haplotype 的东西吗?

【问题讨论】:

  • std::vector&lt;std::variant&lt;A, B, C, D&gt;&gt; 会满足您的需求吗? en.cppreference.com/w/cpp/utility/variant
  • 只要每种类型的大小相似,一个变体就可以获胜。如果不是,空向量的开销通常在两个指针和一个 size_t 左右。
  • std::variant 会为向量中的 每个 元素带来一些恒定的开销。
  • 我认为这个问题有点不明确。你会有多少个单倍型? A、B、C、...等的尺寸是多少?你希望用它们做什么? std::vector&lt;int&gt; 在我的系统上是 24 字节。如果您真的有如此多的单倍型,以至于每个单倍型的 288 字节开销是不可接受的,那么您可能会想要一些手动优化的东西,将数据访问模式考虑在内...
  • @Remi.b A、B、C等有哪些类型?

标签: c++ algorithm design-patterns


【解决方案1】:

我认为无论你做什么,你都必须为即将到来的数据保留一些空间。

向量有可能“浪费”一些未使用的空间。另外 - 对象本身使用一些内存,就像你说的那样。

但是,当您使用数组时,您可以完全控制。 使用std::vector&lt;Gene&gt;[] genes;,您可以完全使用您需要的内存。 但是现在您必须知道哪个索引代表哪种类型(A、B、C...)。该信息将再次使您失去记忆... 此外,如果您想添加另一个数组,您必须复制并重新分配数组。

您可以通过使用指针来避免使用为每个向量分配的内存并使用nullptr 对其进行初始化。然后你按需分配它们。 这样你只会浪费指针的空间。 您当前实现它的方式将初始化每个矢量对象(以及可能的一些保留空间)。

【讨论】:

    【解决方案2】:

    我不知道这是否是您要查找的内容,但我认为,您可以存储一个仅包含有用向量的向量,而不是存储只有一个或两个有用的 12 个向量(使用多态性) .

    例如,您为您的基因类型创建一个基类:

    class Gene
    {
        // Make it pure virtual (abstract) for example
    };
    

    然后你创建你的 12 种不同类型的基因来继承这个基类:

    class A : public Gene // Gene type number 1
    {
        // ...
    };
    
    ...
    
    class L : public Gene // Gene type number 12
    {
    
    };
    

    所以你的12种基因都是Gene

    然后,在Haplotype中,可以将有用的基因存储如下:

    class Haplotype
    {
        std::vector<std::vector<Gene*>> genes;
    };
    

    这样,您只存储载体中有用的基因,而不存储其他任何内容。当然,这种使用多态性的设计意味着您存储的是指针而不是值。

    您可以在Haplotype 中添加一个方法,该方法检索其组件的真实类型(成功的 dynamic_cast),以使其从“用户方”无法察觉。

    我不知道这个解决方案是否对你有好处,但我希望它能有所帮助。


    编辑:

    如果你在 C++17 之前无法使用std::variant,我认为这可能是一个替代方案。

    【讨论】:

    • 这需要每个基因的持续开销。 (由于您需要在堆上分配它, 将指向它的指针存储在其中一个向量中)。这比变体更好吗?
    • 我从来没有假装它比变体更好。这只是一个设计方案。如果它不适合 OP 的需要,他会告诉我。不要那么咄咄逼人,我只是想帮忙。
    • @BrennanVincent 变体至少是最大元素的大小(+ 一些用于标记)。如果存在大小差异大于 ptr 大小的“基因”,则需要更少的内存。但是,真正的问题应该是“这如何比 OP 当前的实现更好”。这和 std::variant 都不值得。
    【解决方案3】:

    如果所有基因类型的大小大致相同,那么这样做可能是一种胜利:

    union Gene
    {
        A a;
        B b;
        C c;
        D d;
    };
    class Haplotype
    {
        std::vector<Gene> genes;
        int aEnd, bEnd, cEnd;
      public:
        A getA(int idx) {
            return genes[idx].a;
        }
        B getB(int idx) {
            return genes[idx + aEnd].b;
        }
        C getC(int idx) {
            return genes[idx + bEnd].c;
        }
        D getD(int idx) {
            return genes[idx + cEnd].d;
        }
    };
    

    只要您不关心单个类型的顺序,就可以在恒定时间内将给定类型的新基因添加到载体中(您无需将所有内容都推回)。

    例如,如果您需要添加一个 B,您可以将它移动到第一个 C 的位置,然后将它移动到第一个 D 的位置,然后将其推到最后。然后,您将增加 bEndcEnd 中的每一个。

    例如:

    最初:[A1, A2, A3, B1, B2, C1, C2, C3, C4, D1, D2, D3, D4, D5]

    在 bEnd 添加新的 B:[A1, A2, A3, B1, B2, B3, C2, C3, C4, D1, D2, D3, D4, D5]

    替换被驱逐的C:[A1, A2, A3, B1, B2, B3, C2, C3, C4, C1, D2, D3, D4, D5]

    在末尾添加被驱逐的 D:[A1, A2, A3, B1, B2, B3, C2, C3, C4, C1, D2, D3, D4, D5, D1]

    要在恒定时间内移除一个基因,你可以反过来做类似的事情。关键是要保持所有 As 在所有 Bs 之前,在所有 Cs 之前,在所有 Ds 之前的不变量。

    这种方案可以让您以更少的开销摆脱困境:每种类型只有一个 int(用于存储结束索引),以换取更丑陋的代码,添加或删除基因时的一些额外时间开销,以及无法保持每组基因的插入顺序。这些缺点是否值得由您决定。

    此外,如果类型具有显着不同的大小,此设计将产生每个元素的开销,因为联合必须至少与其最大成员一样大。

    【讨论】:

      猜你喜欢
      • 2016-02-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多