首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >哈希表的性能,为什么C++是最慢的?

哈希表的性能,为什么C++是最慢的?
EN

Stack Overflow用户
提问于 2015-11-27 05:03:52
回答 4查看 2.7K关注 0票数 7

在散列上运行一个简单的性能测试,看起来C++版本比perl版本和golang版本都慢。

  • perl版本花费了大约200毫秒,
  • C++版本花费了280毫秒。
  • 金刚版花了56毫秒。

在我的电脑上使用Core(TM) i7-2670QM CPU @ 2.20GHz,Ubuntu 14.04.3LTS,

有什么想法吗?

perl版本

代码语言:javascript
复制
use Time::HiRes qw( usleep ualarm gettimeofday tv_interval nanosleep
                      clock_gettime clock_getres clock_nanosleep clock
                      stat );
sub getTS {
    my ($seconds, $microseconds) = gettimeofday;
    return $seconds + (0.0+ $microseconds)/1000000.0;
}
my %mymap;
$mymap{"U.S."} = "Washington";
$mymap{"U.K."} = "London";
$mymap{"France"} = "Paris";
$mymap{"Russia"} = "Moscow";
$mymap{"China"} = "Beijing";
$mymap{"Germany"} = "Berlin";
$mymap{"Japan"} = "Tokyo";
$mymap{"China"} = "Beijing";
$mymap{"Italy"} = "Rome";
$mymap{"Spain"} = "Madrad";
$x = "";
$start = getTS();
for ($i=0; $i<1000000; $i++) {
    $x = $mymap{"China"};
}
printf "took %f sec\n", getTS() - $start;

C++版本

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <unordered_map>
#include <sys/time.h>

double getTS() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + tv.tv_usec/1000000.0;
}
using namespace std;
int main () {
  std::unordered_map<std::string,std::string> mymap;

  // populating container:
    mymap["U.S."] = "Washington";
    mymap["U.K."] = "London";
    mymap["France"] = "Paris";
    mymap["Russia"] = "Moscow";
    mymap["China"] = "Beijing";
    mymap["Germany"] = "Berlin";
    mymap["Japan"] = "Tokyo";
    mymap["China"] = "Beijing";
    mymap["Italy"] = "Rome";
    mymap["Spain"] = "Madrad";  

  double start = getTS();
  string x;
  for (int i=0; i<1000000; i++) {
      mymap["China"];
  }
  printf("took %f sec\n", getTS() - start);
  return 0;
}

Golang版本

代码语言:javascript
复制
package main

import "fmt"
import "time"

func main() {
    var x string
    mymap := make(map[string]string)
    mymap["U.S."] = "Washington";
    mymap["U.K."] = "London";
    mymap["France"] = "Paris";
    mymap["Russia"] = "Moscow";
    mymap["China"] = "Beijing";
    mymap["Germany"] = "Berlin";
    mymap["Japan"] = "Tokyo";
    mymap["China"] = "Beijing";
    mymap["Italy"] = "Rome";
    mymap["Spain"] = "Madrad";
    t0 := time.Now()
    sum := 1
    for sum < 1000000 {
        x = mymap["China"]
        sum += 1
    }
    t1 := time.Now()
    fmt.Printf("The call took %v to run.\n", t1.Sub(t0))
    fmt.Println(x)
}

更新1

为了改进C++版本,将x = mymap["China"];改为mymap["China"];,但性能差别很小。

更新2

在没有任何优化的情况下编译时,我得到了最初的结果:g++ -std=c++11 unorderedMap.cc。使用"-O2“优化,它只需花费大约一半的时间(150 it )。

更新3

为了删除可能的char*string构造函数调用,我创建了一个字符串常量。时间可归结为220 in左右(编译过程中没有优化)。由于@neil的建议,优化(-O2标志)的时间大约是80 is。

代码语言:javascript
复制
  double start = getTS();
  string x = "China";
  for (int i=0; i<1000000; i++) {
      mymap[x];
  }

更新4

感谢@,他指出perl版本存在语法错误。我改了。性能数字约为150毫秒。

更新5

看来执行指令的数量很重要。使用命令valgrind --tool=cachegrind <cmd>

对于Go版本

代码语言:javascript
复制
$ valgrind --tool=cachegrind  ./te1
==2103== Cachegrind, a cache and branch-prediction profiler
==2103== Copyright (C) 2002-2013, and GNU GPL'd, by Nicholas Nethercote et al.
==2103== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==2103== Command: ./te1
==2103== 
--2103-- warning: L3 cache found, using its data for the LL simulation.
The call took 1.647099s to run.
Beijing
==2103== 
==2103== I   refs:      255,763,381
==2103== I1  misses:          3,709
==2103== LLi misses:          2,743
==2103== I1  miss rate:        0.00%
==2103== LLi miss rate:        0.00%
==2103== 
==2103== D   refs:      109,437,132  (77,838,331 rd   + 31,598,801 wr)
==2103== D1  misses:        352,474  (   254,714 rd   +     97,760 wr)
==2103== LLd misses:        149,260  (    96,250 rd   +     53,010 wr)
==2103== D1  miss rate:         0.3% (       0.3%     +        0.3%  )
==2103== LLd miss rate:         0.1% (       0.1%     +        0.1%  )
==2103== 
==2103== LL refs:           356,183  (   258,423 rd   +     97,760 wr)
==2103== LL misses:         152,003  (    98,993 rd   +     53,010 wr)
==2103== LL miss rate:          0.0% (       0.0%     +        0.1%  )

对于C++优化版本(没有优化标志)

代码语言:javascript
复制
$ valgrind --tool=cachegrind ./a.out
==2180== Cachegrind, a cache and branch-prediction profiler
==2180== Copyright (C) 2002-2013, and GNU GPL'd, by Nicholas Nethercote et al.
==2180== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==2180== Command: ./a.out
==2180== 
--2180-- warning: L3 cache found, using its data for the LL simulation.
took 64.657681 sec
==2180== 
==2180== I   refs:      5,281,474,482
==2180== I1  misses:            1,710
==2180== LLi misses:            1,651
==2180== I1  miss rate:          0.00%
==2180== LLi miss rate:          0.00%
==2180== 
==2180== D   refs:      3,170,495,683  (1,840,363,429 rd   + 1,330,132,254 wr)
==2180== D1  misses:           12,055  (       10,374 rd   +         1,681 wr)
==2180== LLd misses:            7,383  (        6,132 rd   +         1,251 wr)
==2180== D1  miss rate:           0.0% (          0.0%     +           0.0%  )
==2180== LLd miss rate:           0.0% (          0.0%     +           0.0%  )
==2180== 
==2180== LL refs:              13,765  (       12,084 rd   +         1,681 wr)
==2180== LL misses:             9,034  (        7,783 rd   +         1,251 wr)
==2180== LL miss rate:            0.0% (          0.0%     +           0.0%  )

对于C++优化版本

代码语言:javascript
复制
$ valgrind --tool=cachegrind ./a.out
==2157== Cachegrind, a cache and branch-prediction profiler
==2157== Copyright (C) 2002-2013, and GNU GPL'd, by Nicholas Nethercote et al.
==2157== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==2157== Command: ./a.out
==2157== 
--2157-- warning: L3 cache found, using its data for the LL simulation.
took 9.419447 sec
==2157== 
==2157== I   refs:      1,451,459,660
==2157== I1  misses:            1,599
==2157== LLi misses:            1,549
==2157== I1  miss rate:          0.00%
==2157== LLi miss rate:          0.00%
==2157== 
==2157== D   refs:        430,486,197  (340,358,108 rd   + 90,128,089 wr)
==2157== D1  misses:           12,008  (     10,337 rd   +      1,671 wr)
==2157== LLd misses:            7,372  (      6,120 rd   +      1,252 wr)
==2157== D1  miss rate:           0.0% (        0.0%     +        0.0%  )
==2157== LLd miss rate:           0.0% (        0.0%     +        0.0%  )
==2157== 
==2157== LL refs:              13,607  (     11,936 rd   +      1,671 wr)
==2157== LL misses:             8,921  (      7,669 rd   +      1,252 wr)
==2157== LL miss rate:            0.0% (        0.0%     +        0.0%  )
EN

回答 4

Stack Overflow用户

发布于 2015-11-27 06:21:45

来自您的Perl代码(在您试图修复它之前):

@mymap = ();$mymap"U.S.“= "Washington";$mymap"U.K.”= "London";

这不是一个地图,而是一个数组。散列映射的语法是:

代码语言:javascript
复制
  %mymap;
  $mymap{"U.S."} = ....

因此,您实际上要做的是创建一个数组,而不是一个哈希映射,并始终访问元素0。请在Perl中始终使用use strict;use warnings;,即使使用带有警告的简单语法检查也会告诉您问题所在:

代码语言:javascript
复制
perl -cw x.pl
Argument "U.S." isn't numeric in array element at x.pl line 9.
Argument "U.K." isn't numeric in array element at x.pl line 10.

除此之外,基准测试的主要部分实际上没有什么有用的(赋值变量,永远不使用它),一些编译器可以检测它并简单地优化它。

如果您检查您的Perl程序生成的代码,您将看到:

代码语言:javascript
复制
$ perl -MO=Deparse x.pl
@mymap = ();
$mymap[0] = 'Washington';
$mymap[0] = 'London';
...
for ($i = 0; $i < 1000000; ++$i) {
    $x = $mymap[0];
}

也就是说,它在编译时检测问题,并将其替换为对数组索引0的访问。

因此,每当需要进行基准测试时:

  • 检查你的程序是否真的做了它应该做的事情。
  • 检查编译器没有优化其他语言将在运行时执行的东西,也没有在编译时执行。没有或可预测结果的任何类型的语句都容易进行这样的优化。
  • 验证你实际上测量的是你想要测量的,而不是其他的东西。即使是对程序的微小更改也会影响运行时,因为需要的内存分配不是以前的等等,这些更改可能与您打算测量的内容无关。在基准测试中,您可以一次又一次地度量对同一个散列元素的访问,而不需要在两者之间访问其他元素。这是一个可以很好地发挥处理器缓存,但可能不反映现实世界的使用活动。

而且,使用一个简单的计时器并不是一个现实的基准。系统上还有其他的进程,有调度程序,有高速缓存.对于今天的CPU来说,它高度依赖于系统上的负载,因为CPU可能会以较低的功率模式运行一个基准测试,即使用不同的CPU时钟。例如,在我的系统上,同一“基准”的多次运行在100到150 my之间的测量时间不同。

基准是谎言,像你这样的微观基准是双重的。

票数 16
EN

Stack Overflow用户

发布于 2015-11-27 10:44:24

我对您的示例做了一些修改,以了解哈希表结构的一些细节:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <unordered_map>
#include <sys/time.h>
#include <chrono>

using namespace std;
int main()
{
    std::unordered_map<std::string, std::string> mymap;

    // populating container:
    mymap["U.S."] = "Washington";
    mymap["U.K."] = "London";
    mymap["France"] = "Paris";
    mymap["Russia"] = "Moscow";
    mymap["China"] = "Beijing";
    mymap["Germany"] = "Berlin";
    mymap["Japan"] = "Tokyo";
    mymap["China"] = "Beijing";
    mymap["Italy"] = "Rome";
    mymap["Spain"] = "Madrad";

    std::hash<std::string> h;
    for ( auto const& i : mymap )
    {

        printf( "hash(%s) = %ud\n", i.first.c_str(), h( i.first ) );
    }

    for ( int i = 0; i != mymap.bucket_count(); ++i )
    {
        auto const bucketsize = mymap.bucket_size( i );
        if ( 0 != bucketsize )
        {
            printf( "size of bucket %d = %d\n", i, bucketsize );
        }
    }

    auto const start = std::chrono::steady_clock::now();

    string const x = "China";
    std::string res;

    for ( int i = 0; i < 1000000; i++ )
    {
        mymap.find( x );
    }

    auto const elapsed = std::chrono::steady_clock::now() - start;
    printf( "%s\n", res );
    printf( "took %d ms\n",
            std::chrono::duration_cast<std::chrono::milliseconds>( elapsed ).count() );
    return 0;
}

在我的系统上运行这个程序,运行时为~68 my,输出如下:

代码语言:javascript
复制
hash(Japan) = 3611029618d
hash(Spain) = 749986602d
hash(China) = 3154384700d
hash(U.S.) = 2546447179d
hash(Italy) = 2246786301d
hash(Germany) = 2319993784d
hash(U.K.) = 2699630607d
hash(France) = 3266727934d
hash(Russia) = 3992029278d
size of bucket 0 = 0
size of bucket 1 = 0
size of bucket 2 = 1
size of bucket 3 = 1
size of bucket 4 = 1
size of bucket 5 = 0
size of bucket 6 = 1
size of bucket 7 = 0
size of bucket 8 = 0
size of bucket 9 = 2
size of bucket 10 = 3

事实证明,哈希表没有得到很好的优化,并且包含了一些冲突。进一步打印桶中的元素,显示西班牙和中国在桶9中。桶可能是一个链表,节点分布在内存中,说明性能下降。

如果您选择了另一个哈希表大小,这样就没有冲突,就可以得到加速。我通过添加mymap.rehash(1001)对其进行了测试,使我的速度提高了20-30%,达到了44-52 to之间的水平。

现在,另一点是计算“中国”的哈希值。该函数在每次迭代中执行。当我们切换到常量的普通C字符串时,我们可以消除这种情况:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <unordered_map>
#include <sys/time.h>
#include <chrono>

static auto constexpr us = "U.S.";
static auto constexpr uk = "U.K.";
static auto constexpr fr = "France";
static auto constexpr ru = "Russia";
static auto constexpr cn = "China";
static auto constexpr ge = "Germany";
static auto constexpr jp = "Japan";
static auto constexpr it = "Italy";
static auto constexpr sp = "Spain";

using namespace std;
int main()
{
    std::unordered_map<const char*, std::string> mymap;

    // populating container:
    mymap[us] = "Washington";
    mymap[uk] = "London";
    mymap[fr] = "Paris";
    mymap[ru] = "Moscow";
    mymap[cn] = "Beijing";
    mymap[ge] = "Berlin";
    mymap[jp] = "Tokyo";
    mymap[it] = "Rome";
    mymap[sp] = "Madrad";

    string const x = "China";
    char const* res = nullptr;
    auto const start = std::chrono::steady_clock::now();
    for ( int i = 0; i < 1000000; i++ )
    {
        res = mymap[cn].c_str();
    }

    auto const elapsed = std::chrono::steady_clock::now() - start;
    printf( "%s\n", res );
    printf( "took %d ms\n",
            std::chrono::duration_cast<std::chrono::milliseconds>( elapsed ).count() );
    return 0;
}

在我的机器上,这将运行时减少了50%至20 my。不同之处在于,它现在不是从字符串内容中计算哈希值,而是将地址转换为一个哈希值,而哈希值的速度要快得多,因为它只是将地址值作为size_t返回。我们也不需要重新散列,因为存储桶与cn之间没有冲突。

票数 4
EN

Stack Overflow用户

发布于 2015-11-27 07:14:05

这仅仅表明,对于这个特定的用例来说,Go哈希映射实现是非常优化的。

mymap["China"]调用专门为字符串键优化的快件。特别是对于小的单桶映射,哈希代码甚至不计算短字符串(小于32字节)。

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/33950565

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档