【问题标题】:Most efficient way to assign values to maps为地图赋值的最有效方法
【发布时间】:2012-12-22 11:40:22
【问题描述】:

哪种方式为地图赋值最有效?还是它们都针对相同的代码进行了优化(在大多数现代编译器上)?

   // 1) Assignment using array index notation
   Foo["Bar"] = 12345;

   // 2) Assignment using member function insert() and STL pair
   Foo.insert(std::pair<string,int>("Bar", 12345));

   // 3) Assignment using member function insert() and "value_type()"
   Foo.insert(map<string,int>::value_type("Bar", 12345));

   // 4) Assignment using member function insert() and "make_pair()"
   Foo.insert(std::make_pair("Bar", 12345));

(我知道我可以对编译器输出进行基准测试和检查,但是现在出现了这个问题,我手边唯一的东西就是我的手机......呵呵)

【问题讨论】:

  • 我会赌they are all the same。我可以详细说明为什么一个会比另一个更快,但前提是我们忽略编译器优化。
  • @BoPersson:实际上,4) 有效,std::pair&lt;std::string, int&gt; p = std::make_pair("Bar", 12345); 因为pair 的构造函数是允许的(只要类型可以转换,它就有效)。

标签: c++ dictionary std stdmap c++-standard-library


【解决方案1】:

首先,[]insert 之间存在语义差异:

  • []替换旧值(如果有)
  • insert忽略新值(如果旧值已经存在)

因此一般来说比较两者是没有用的。

关于插入版本:

  • std::map&lt;std::string, int&gt;::value_type std::pair&lt;std::string const, int&gt; 所以 3 和 4 之间没有(重要)区别
  • std::make_pair("Bar", 12345)std::pair&lt;std::string, int&gt;("Bar", 12345) 便宜,因为 std::string 类型是一个完整的类,可以对复制进行操作,而 "Bar" 只是一个字符串文字(因此只是一个指针副本);但是,由于最后您确实需要创建 std::string... 这将取决于您的编译器的质量

一般来说,我会推荐:

  • []更新中
  • insert(std::make_pair()) 忽略重复项

std::make_pair 不仅更短,而且还尊重 DRY 准则:不要重复自己。


不过,为了完整起见,最快(也是最简单)的是 emplace(启用 C++11):

map.emplace("Bar", 12345);

它的行为类似于insert,但它在原地构造新元素。

【讨论】:

  • 我很清楚[]将替换任何现有值。大多数数据结构上的这种表示法也是如此。所以我在想一张干净的地图,只插入一个值。但由于其他人实际上确实跟踪是否存在旧值,我猜想必须为此目的生成一些额外的代码。从而使它们与[] 不同。
  • @inquam: [] 可能会慢一点,因为它涉及构建一个空值并立即丢弃它...但是,如果构建空值在第一名(显然)。
  • @inquam:insert 的典型实现将:1) 使用内部查找函数来获取树节点,如果有的话 2) 如果存在,则返回 else 3) 使用find 插入新值并返回。 operator[] 的实现将做完全相同的事情,除了 3) 插入一个默认构造值并返回它。 (然后您会覆盖返回的任何内容。)
  • std::map&lt;std::string, int&gt;::value_typestd::pair&lt;const std::string, int&gt;,而不是 std::pair&lt;std::string, int&gt;。因此只有 3) 具有可以直接插入的类型,无需额外的类型转换(或[] 的开销)。编译器可能会消除这种开销,但总的来说我不会指望它。
  • @Grizzly:如果我们走这条路,emplace 会更好 -> 仅在必要时就地施工。
【解决方案2】:

1) 可能比其他方法稍慢,因为std::map::operator[] 首先默认创建对象,如果它不存在,然后返回一个引用,您可以使用operator= 设置您想要的值,即两个操作。

2-4) 应该是等价的,因为map::value_type 是相同类型的std::pair 的typedef,因此make_pair 也是等价的。编译器应该对它们一视同仁。

还请注意,如果您需要检查是否存在(例如根据是否存在执行特殊逻辑)然后还要插入它,则可以进一步提高性能,方法是使用 map::lower_bound 首先获得提示元素应该在哪里,所以map::insert 不必再次搜索整个map

 // get the iterator to where the key *should* be if it existed:
 std::map::iterator hint = mymap.lower_bound(key);

 if (hint == mymap.end() || mymap.key_comp()(key, hint->first)) {
     // key didn't exist in map
     // special logic A here...

     // insert at the correct location
     mymap.insert(hint, make_pair(key, new_value));
 } else { 
     // key exists in map already
     // special logic B here...

     // just update value
     hint->second = new_value;
 }

【讨论】:

  • 应该有!在 mymap.key_comp()(key, hint->first) 之前?
  • @ManoharReddyPoreddy 没有; mymap.key_comp()(key, hint-&gt;first) 检查key 是否小于hint-&gt;first。如果true,这意味着key 不存在(因为lower_bound 返回一个迭代器到第一个元素不少于)。 false 意味着keyhint-&gt;first 相同。
  • 有点混乱。 lower_bound 可以给出 >=,而不是 first) && mymap.key_comp()(hint->first, key ) 或类似的?
  • 我同意一开始会让人困惑。就像你说的,lower_bound 将迭代器返回到现有元素 等于大于 提供的 key 意味着 hint-&gt;first &lt; key 将始终是 @987654344 @;没有必要检查这个。相反,只需检查key &lt; hint-&gt;first 就可以让我们确认返回的迭代器是等于(key &lt; hint-&gt;first) == false)还是更大(key &lt; hint-&gt;first) == true),即key 是否已经是否存在于地图中。这涵盖了所有情况。
  • 实际上我在最近几天失去了上下文,因为我正在处理其他事情。我赶紧查了一下,前5个链接都没有用它作为比较的值,但是作为cplusplus.com/reference/map/map/key_comptutorialspoint.com/cpp_standard_library/…的范围,我不是说你错了,只是看不清楚你在说什么.
【解决方案3】:

您的第一种可能性:Foo["Bar"] = 12345; 的语义与其他的有些不同——如果不存在(与其他对象一样),它将插入一个新对象,但如果不存在,则覆盖当前内容(如果该密钥已经存在,其他使用insert 的人将失败)。

就速度而言,它有可能比其他的要慢。当您插入一个新对象时,它会创建一个具有指定键和默认构造的 value_type 的对,然后分配正确的 value_type。其他人都构造了正确的键和值对并插入该对象。然而,公平地说,我的经验是差异很少显着(对于较旧的编译器,它更显着,但对于较新的编译器则非常小)。

接下来的两个是等价的。您只是使用两种不同的方式来命名同一类型。在运行时,它们之间根本没有区别。

第四个使用模板函数(make_pair),理论上可能涉及额外级别的函数调用。不过,如果您仔细确保编译器完全没有优化(尤其是内联),我会很惊讶地看到与此有真正的区别。

底线:第一个通常会比其他的慢一点(但并非总是如此,也不会慢很多)。其他三个几乎总是相等的(例如:通常期望任何合理的编译器为所有三个生成相同的代码),即使第四个更慢有理论上的理由。

【讨论】:

    【解决方案4】:

    尽管已经有几个很好的答案,但我认为我不妨做一个快速基准测试。每一个运行 500 万次,并使用 c++11 的 chrono 来测量它所花费的时间。

    代码如下:

    #include <string>
    #include <map>
    #include <chrono>
    #include <cstdio>
    
    // 5 million
    #define times 5000000
    
    int main()
    {
        std::map<std::string, int> foo1, foo2, foo3, foo4, foo5;
        std::chrono::steady_clock::time_point timeStart, timeEnd;
        int x = 0;
    
        // 1) Assignment using array index notation
        timeStart = std::chrono::steady_clock::now();
        for (x = 0; x <= times; x++)
        {
            foo1[std::to_string(x)] = 12345;
        }
        timeEnd = std::chrono::steady_clock::now();
        printf("1) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());
    
        // 2) Assignment using member function insert() and STL pair
        timeStart = std::chrono::steady_clock::now();
        for (x = 0; x <= times; x++)
        {
            foo2.insert(std::pair<std::string, int>(std::to_string(x), 12345));
        }
        timeEnd = std::chrono::steady_clock::now();
        printf("2) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());
    
        // 3) Assignment using member function insert() and "value_type()"
        timeStart = std::chrono::steady_clock::now();
        for (x = 0; x <= times; x++)
        {
            foo3.insert(std::map<std::string, int>::value_type(std::to_string(x), 12345));
        }
        timeEnd = std::chrono::steady_clock::now();
        printf("3) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());
    
        // 4) Assignment using member function insert() and "make_pair()"
        timeStart = std::chrono::steady_clock::now();
        for (x = 0; x <= times; x++)
        {
            foo4.insert(std::make_pair(std::to_string(x), 12345));
        }
        timeEnd = std::chrono::steady_clock::now();
        printf("4) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());
    
        // 5) Matthieu M.'s suggestion of C++11's emplace
        timeStart = std::chrono::steady_clock::now();
        for (x = 0; x <= times; x++)
        {
            foo5.emplace(std::to_string(x), 12345);
        }
        timeEnd = std::chrono::steady_clock::now();
        printf("5) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());
    
        return 0;
    }
    

    500 万次迭代的输出为:

    1) took 23448 milliseconds
    2) took 22854 milliseconds
    3) took 22372 milliseconds
    4) took 22988 milliseconds
    5) took 21356 milliseconds
    

    GCC 版本:

    g++ (Built by MinGW-builds project) 4.8.0 20121225 (experimental)
    

    我的机器:

    Intel i5-3570k overclocked at 4.6 GHz
    

    EDIT1:修复代码并重新进行基准测试。

    EDIT2:添加了 Matthieu M. 对 C++11 的 emplace 的建议,他是对的,emplace 最快

    【讨论】:

    • 整洁...但是错了。请注意,这里的insert 版本永远不会实际插入,而[] 版本总是分配。所以这开始就搞砸了......
    • @Matthieu M. 将代码更改为每次插入不同的点,这样更好吗?
    • 我认为代码仍然有两个问题:1)您可能应该清除循环之间的映射,因为现在只有第一个循环的方式实际上会 insert 任何东西 2)创建一个 @ 987654328@ 相当昂贵,因此可能会影响测量结果。 std::to_string 可能是一个影响较小的选项(考虑到最终的地图大小,我不完全确定这是否会产生影响,但最好确定一下)。
    • @Grizzly 修改它以反映您的建议并重新进行测试。
    • 我不确定我是否相信这种测试方法。我知道 2) 最快,9946 毫秒,5) 最长 14557 毫秒。 1,3,4) 都在 10000 毫秒左右,和 2) 一样。
    【解决方案5】:

    如果该关键位置没有对象,则:

    std::map::emplace 是最有效的。 insert 是第二个(但会非常接近)。 [] 效率最低。

    [],如果那里没有对象,平凡构造一个。然后它调用operator=

    insertstd::pair 参数执行复制构造函数调用。

    但是,在地图的情况下,map.insert( make_pair( std::move(key), std::move(value) ) ) 将接近于map.emplace( std::move(key), std::move(value) )

    如果关键位置有对象,则[] 将调用operator=,而insert/emplace 将销毁旧对象并创建一个新对象。在这种情况下,[] 可能会更便宜。

    最后,这取决于您的 operator= 与复制构造、琐碎构造与析构函数对您的密钥和价值的成本。

    std::map 的树结构中查找内容的实际工作将非常接近相同,这并不好笑。

    【讨论】:

      【解决方案6】:

      第三个是最佳选择(恕我直言),但 2、3 和 4 是相等的。

      // 3) Assignment using member function insert() and "value_type()"
      Foo.insert(map<string,int>::value_type("Bar", 12345));
      

      为什么我认为第三个是最好的选择:您正在执行 只有一个 操作来插入值:只需插入(好吧,也有搜索)并且您可以知道该值是否已插入,检查返回值的 second 成员,并且实现授权不覆盖该值。

      value_type 的使用也有好处:您不需要知道映射类型或键类型,因此对模板编程很有用。

      最糟糕的(恕我直言)是第一个:

      // 1) Assignment using array index notation
      Foo["Bar"] = 12345;
      

      您正在调用std::map::operator[],它创建一个对象并返回对它的引用,然后调用映射对象operator =。您正在为插入执行两项操作:首先是插入,其次是分配。

      还有一个问题:你不知道值是被插入还是被覆盖。

      【讨论】:

      • 3)2) 快多少?
      • 它们是等价的(正如其他答案中所说)我只关注其中一个是最好的 ...所以我想我错过了这一点我的回答。
      • 2,3,4 可以相等,提供一个好的优化器。然而,在未优化的情况下,3) 应该是最快的,因为 2) 和 4) 都会导致额外的类型转换为 std::pair&lt;const std::string, int&gt;
      猜你喜欢
      • 1970-01-01
      • 2017-09-07
      • 2015-07-09
      • 2014-12-18
      • 1970-01-01
      • 2013-03-08
      • 1970-01-01
      • 1970-01-01
      • 2016-05-11
      相关资源
      最近更新 更多