【问题标题】:hashing a 32bit integer is slower than bitwise operation on hash of 3 16bit integers?散列一个 32 位整数比对 3 个 16 位整数散列的按位运算慢?
【发布时间】:2019-09-06 00:48:27
【问题描述】:

我正在开发对延迟敏感的软件。今天,我决定通过在联合中使用 BitField 结构来使我的结构更小,并将该 BitField 结构读取为一个整数,我将其用作我的哈希函数的唯一键。我期望从唯一键上的散列获得比对我的类的属性散列的按位运算稍微(如果不是很多)更好的性能,但结果证明进行按位运算来获取散列键要快得多?我一直试图找出原因,但我无法得出任何结论。

这是我的代码:http://quick-bench.com/OkNuVOxdVtW8g7OWUXjqcvYlNrs

#include <functional>

struct GroupTupleInformation {
  unsigned int src : 12;
  unsigned int dst : 12;
  unsigned int flow_id : 8;
};

union GroupTupleData {
  GroupTupleInformation info;
  uint32_t hash_key;
};

struct GroupTuple {
  constexpr void setSrc(uint16_t s) noexcept { data.info.src = s; }

  constexpr uint16_t getSrc() const noexcept { return data.info.src; }

  constexpr void setDst(uint16_t d) noexcept { data.info.dst = d; }

  constexpr uint16_t getDst() const noexcept { return data.info.dst; }

  constexpr void setFlowId(uint8_t f) noexcept { data.info.flow_id = f; }

  constexpr uint8_t getFlowId() const noexcept { return data.info.flow_id; }

  constexpr uint32_t getHashKey() const noexcept { return data.hash_key; }

 private:
  GroupTupleData data;
};

struct UnionHashFn {
  constexpr std::size_t operator()(const GroupTuple& gtup) const noexcept {
    return gtup.getHashKey();
  }
};

struct CombinedHashFn {
  std::size_t operator()(const GroupTuple& gtup) const noexcept {
    std::hash<uint16_t> hasher{};
    return hasher(gtup.getSrc()) ^ hasher(gtup.getDst()) ^
           hasher(gtup.getFlowId());
  }
};


static void UnionHashFnBench(benchmark::State& state) {
  unsigned long i = 0;
  UnionHashFn hasher;
  GroupTuple group_tuple;

  size_t key = 0;
  for (auto _ : state) {
    group_tuple.setSrc(i++ % 32);
    group_tuple.setDst(i++ % 32);
    group_tuple.setFlowId(i++ % 32);
    key = hasher(group_tuple);
    benchmark::DoNotOptimize(key);
  }
}
BENCHMARK(UnionHashFnBench);

static void CombinedHashFnBench(benchmark::State& state) {
  unsigned long i = 0;
  CombinedHashFn hasher;
  GroupTuple group_tuple;

  size_t key = 0;
  for (auto _ : state) {
    group_tuple.setSrc(i++ % 32);
    group_tuple.setDst(i++ % 32);
    group_tuple.setFlowId(i++ % 32);
    key = hasher(group_tuple);
    benchmark::DoNotOptimize(key);
  }
}
BENCHMARK(CombinedHashFnBench);

PS:我知道基准化映射或 unordered_map 比对哈希函数的成本进行基准测试更复杂,而且我也知道我为从班级中获取哈希所做的按位操作并不是获取哈希的好方法,只是为了举个简单的例子

【问题讨论】:

  • 需要 extra 个 CPU 周期才能从内存字中解压缩位域。要更新位域,您必须读取内存字,然后写入它,只需选定的位已更新;而不是简单地写一个记忆词。
  • 哦哦。位域。这些似乎总是一个好主意,但回想起来很少会以这种方式结束。为什么不是简单的位掩码?至少这样你就知道内部发生了什么,而不是隐藏太多。
  • @tadman 因为我的两个队友不习惯并且害怕位掩码,所以我使用位域来获得易于理解的界面。
  • 啊,我想他们会学会害怕位域而不是位掩码。除非您有数十亿个这样的东西,否则可能不值得麻烦。您是否对非位域版本(只是一个普通结构)进行了基准测试,以查看它在速度和内存使用方面的表现?它会将您推向 64 位值,但这不会是现代系统的世界末日。
  • “因为我的两个队友不舒服并且害怕位掩码”您正在开发低延迟软件而队友对位掩码感到害怕?真的吗?缓存行怎么样,这让他们疯狂?

标签: c++ performance-testing benchmarking compiler-optimization


【解决方案1】:

std::hash&lt;uint16_t&gt; 只是身份(老实说,这是有问题的),所以表达式:

hasher(gtup.getSrc()) ^ hasher(gtup.getDst()) ^ hasher(gtup.getFlowId())

将被优化为一个非常简单的表达式。它的代码只是:

   lea    -0x2(%rax),%ecx
   lea    -0x1(%rax),%edx
   xor    %ecx,%edx
   xor    %eax,%edx
   and    $0x1f,%edx

另一方面,计算hash_key 需要更多的努力:

   mov    %edx,%esi
   and    $0x1f,%esi
   mov    %ecx,%edi
   and    $0x1f000,%edi
   or     %rsi,%rdi
   add    $0x3,%rdx
   mov    %eax,%esi
   and    $0x1f000000,%esi
   or     %rdi,%rsi

因此hash_key 方法速度较慢也就不足为奇了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-01-27
    • 1970-01-01
    • 1970-01-01
    • 2013-07-20
    • 2011-07-30
    • 2017-12-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多