std :: map in class:在执行速度和内存使用之间进行权衡

时间:2016-03-16 15:04:36

标签: c++

我的问题涉及在设计一个将被实例化数千或数百万次并在不同上下文中使用不同的类时执行速度和内存使用之间的权衡。

所以我有一个包含一堆数字属性的类(存储在int和double中)。一个简单的例子是

class MyObject
{
  public:
    double property1;
    double property2;
    ...
    double property14
    int property15;
    int property16;
    ...
    int property25;
    MyObject();
    ~MyObject();
};

此类由实例化

的不同程序使用
std::vector<MyObject> SetOfMyObjects;

可能包含数百万个元素。问题是,根据上下文,一些或许多属性可能仍然未使用(我们不需要在这个给定的上下文中计算它们),这意味着分配了数百万无用的int和double的内存。正如我所说,属性的有用性和无用性取决于上下文,我想避免为每个特定的上下文编写不同的类。

所以我在考虑使用std :: maps来为我使用的属性分配内存。例如

class MyObject
{
  public:
    std::map<std::string, double> properties_double;
    std::map<std::string, int> properties_int;
    MyObject();
    ~MyObject();
};

如果必须计算“property1”,它将存储为

MyObject myobject;
myobject.properties_double["property1"] = the_value;

显然,我会定义正确的“set”和“get”方法。

据我所知,访问std :: map中的元素作为其大小的对数,但由于属性的数量非常小(大约25),我认为这不应该减慢代码的执行速度得多。

我是否过度思考过这种情况?你认为使用std :: map是个好主意吗?任何来自更多经验丰富的程序员的建议都将受到赞赏。

6 个答案:

答案 0 :(得分:10)

我认为这不是最佳选择,对于25个元素,在查找性能方面使用地图不会带来太多好处。此外,它取决于你将拥有什么类型的属性,如果它是一个固定的属性集,如你的例子,然后字符串查找将浪费内存和CPU周期,你可以去一个所有属性的枚举或者只是一个整数,并为每个元素具有的属性使用顺序容器。对于如此少量的可能属性,由于缓存友好性和整数比较,查找时间将低于映射,并且内存使用也将更低。对于这么小的一组属性,这个解决方案稍微好一些。

然后问题是int通常是double的两倍。它们是不同的类型。因此,不能直接将它们存储在单个容器中,但是您可以在每个元素中有足够的空间容纳double,并使用union或只读/写{{1}如果属性&#34;索引&#34;来自/到int的地址大于14。

所以你可以有一些简单的东西:

double

对于struct Property { int type; union { int d_int; double d_double; }; }; class MyObject { std::vector<Property> properties; }; 1 - 14,您阅读了type字段,d_double 15 - 25 type字段。

<强>基准!!!

出于好奇,我做了一些测试,创建了250k个对象,每个对象有5个int和5个双属性,使用向量,地图和属性的哈希,以及测量的内存使用和设置和获取属性所花费的时间,连续3次运行每个测试以查看对缓存的影响,计算getter的校验和以验证一致性,以下是结果:

d_int

正如预期的那样,矢量解决方案是迄今为止最快且最有效的,尽管它受冷缓存影响最大,即使运行冷,它也比映射或散列实现快。

在冷启动时,矢量实现比地图快16.15倍,比散列快14.75倍。在温暖的运行中,它甚至更快 - 分别快61倍和54倍。

至于内存使用情况,矢量解决方案效率也更高,使用的内存比地图解决方案少4倍,几乎是哈希解决方案的5倍。

正如我所说,它稍微好一点。

澄清,&#34;冷跑&#34;不仅是第一次运行,而且是在属性中插入实际值的运行,因此它很好地说明了插入操作开销。没有任何容器使用预分配,因此他们使用了默认的扩展策略。至于内存使用情况,有可能它没有准确地反映实际内存使用率100%,因为我使用整个工作集来执行可执行文件,并且通常在操作系统级别上也会发生一些预分配,它随着工作集的增加,很可能会更加保守。最后但并非最不重要的一点是,地图和散列解决方案是使用字符串查找实现的,因为OP原本打算这样做,这就是它们如此低效的原因。使用整数作为地图中的键和哈希产生了更具竞争力的结果:

vector | iteration | memory usage MB | time msec | checksum 
setting 0 32 54
setting 1 32 13
setting 2 32 13
getting 0 32 77 3750000
getting 1 32 77 3750000
getting 2 32 77 3750000

map | iteration | memory usage MB | time msec | checksum 
setting 0 132 872
setting 1 132 800
setting 2 132 800
getting 0 132 800 3750000
getting 1 132 799 3750000
getting 2 132 799 3750000

hash | iteration | memory usage MB | time msec | checksum 
setting 0 155 797
setting 1 155 702
setting 2 155 702
getting 0 155 705 3750000
getting 1 155 705 3750000
getting 2 155 706 3750000

哈希和地图的内存使用率要低得多,同时仍高于矢量,但就性能而言,表格会被转换,而矢量解决方案窗口会在插入时获胜,在读取和写入时,地图解决方案会获得奖杯。所以有权衡。

与将所有属性作为对象成员相比,保存了多少内存,通过粗略计算,在顺序容器中拥有250k这样的对象需要大约80 MB的RAM。所以你为矢量解决方案保存了50 MB,而哈希解决方案几乎没有。毫无疑问 - 直接成员访问速度会快得多。

答案 1 :(得分:7)

TL; DR:它不值得。

我们得到的木匠:测量两次,切一次。应用它。

您的25 intdouble将占用x86_64处理器:

  • 14 double:112字节(14 * 8)
  • 11 int:44字节(11 * 4)

总计156个字节。

在大多数实施中,std::pair<std::string, double>将消耗:

  • 字符串
  • 的24个字节
  • 双重
  • 的8个字节

std::map<std::string, double>中的节点将添加至少3个指针(1个父节点,2个子节点)和另一个24个字节的红黑标记。

每个属性至少56个字节

即使使用0开销分配器,每次在此map中存储3个或更多元素时,您使用的字节数超过156个......

压缩(类型,属性)对将占用:

  • 属性的8个字节(double是最糟糕的情况)
  • 该类型的8个字节(您可以选择较小的类型,但对齐启动)

每对总共16个字节。比map好多了。

存储在vector中,这意味着:

  • vector
  • 的24字节开销
  • 每个属性16个字节

即使使用0开销分配器,每次在此vector中存储9个或更多元素时,您使用的字节数超过156个。

您知道解决方案:拆分该对象。

答案 2 :(得分:6)

您正在按名称查找对象,您知道它们会在那里。所以按名称查找它们。

  

据我所知,访问std :: map中的元素作为其大小的对数,但由于属性的数量非常小(大约25),我认为这不应该减慢代码的执行速度得多。

您的计划速度将超过一个数量级。查找地图可能是O(logN)但是它的O(LogN)* C.与直接访问属性(慢几千倍)相比,C将巨大

  

暗示数百万无用的int和double的内存被分配

在我能想到的所有实现中,std::string至少为24个字节 - 假设您热衷于简短的属性名称(google&#39;短字符串优化&#39;以获取详细信息)。

除非60%的属性未填充,否则根本不会使用字符串键入的地图进行保存。

答案 3 :(得分:4)

每个对象和小地图对象都有可能会遇到另一个问题 - 内存碎片。它可以用std::vector代替std::pair<key,value>并进行查找(我认为二进制搜索应该足够了,但这取决于你的情况,做线性查找可能更便宜但不是对矢量进行排序)。对于属性键,我将使用枚举而不是字符串,除非稍后由接口(您没有显示)指示。

答案 4 :(得分:2)

只是一个想法(未编译/测试):

struct property_type
{
  enum { kind_int, kind_double } k;
  union { int i; double d; };
};

enum prop : unsigned char { height, widht, };

typedef std::map< std::pair< int/*data index*/, prop/*property index*/ >, property_type > map_type;

class data_type
{
  map_type m;

public:

  double& get_double( int i, prop p )
  {
    // invariants...
    return m[ std::pair<int,prop>(i,p) ].d; 
  }

};

答案 5 :(得分:2)

数百万的整数和双数仍然只有数百兆字节的数据。在现代计算机上可能不是一个大问题。

地图路线看起来似乎是浪费时间,但有一种替代方案可以节省内存,同时保留不错的性能特征:将细节存储在单独的向量中并将索引存储到此向量中(或-1)对于主数据类型的未分配)。遗憾的是,您的描述并未真正指出属性使用情况的实际外观,但我猜测您可以细分为始终或通常设置在一起的属性以及每个节点所需的属性。让我们假设您细分为四组:A,B,C和D.每个节点都需要As,而B,C和D很少设置,但所有元素通常一起修改,然后修改结构#&# 39;像这样重新存储:

struct myData {
  int A1;
  double A2;
  int B_lookup = -1;
  int C_lookup = -1;
  int D_lookup = -1;
};

struct myData_B {
  int B1;
  double B2;
  //etc.
};

// and for C and D

然后在主类中存储4个向量。当访问Bs中的属性时,向Bs的向量添加新的myData_B(实际上deque可能是更好的选择,保留快速访问但没有相同的内存碎片问题)并设置{{1}原始B_lookup中的值为新myData的索引。对于Cs和Ds也一样。

这是否值得做,取决于您实际访问的属性有多少以及如何一起访问它们,但您应该能够根据自己的喜好修改该想法。