blob: 9bed3c46efb0639b343152021195222f8adb9131 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include <stdint.h>
#include <iostream>
#include <limits>
#include <memory>
#include <vector>
#include <immintrin.h>
#include "util/benchmark.h"
#include "util/cpu-info.h"
#include "util/hash-util.h"
#include "util/sse-util.h"
using namespace std;
using namespace impala;
// Test hash functions that take integers as arguments and produce integers as the result.
//
// Machine Info: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
// 32 -> 32: Function iters/ms 10%ile 50%ile 90%ile 10%ile 50%ile 90%ile
// (relative) (relative) (relative)
// ---------------------------------------------------------------------------------------------------------
// FNV 25.7 26 26.4 1X 1X 1X
// Zobrist 30.2 30.7 30.9 1.18X 1.18X 1.17X
// MultRot 42.6 43.4 43.4 1.66X 1.67X 1.64X
// MultiplyAddShift 42.4 43.2 43.2 1.65X 1.66X 1.64X
// Jenkins1 51.8 54 54 2.02X 2.08X 2.05X
// Jenkins2 66.2 67.4 67.5 2.58X 2.59X 2.56X
// CRC 98.6 100 101 3.84X 3.85X 3.84X
// MultiplyShift 150 152 153 5.84X 5.86X 5.79X
//
// 32 x 4 -> 32 x 4: Function iters/ms 10%ile 50%ile 90%ile 10%ile 50%ile 90%ile
// (relative) (relative) (relative)
// ---------------------------------------------------------------------------------------------------------
// (Multiple<Zobrist, 4>) 30.8 31.2 31.4 1X 1X 1X
// (Multiple<MultiplyAddShift, 4>) 44.2 45 45 1.43X 1.44X 1.43X
// (Multiple<CRC, 4>) 118 120 121 3.84X 3.86X 3.85X
// (Multiple<MultiplyShift, 4>) 156 159 159 5.07X 5.1X 5.08X
// MultiplyAddShift128 75.7 77.2 77.2 2.46X 2.48X 2.46X
// MultiplyShift128 128 131 133 4.16X 4.21X 4.23X
//
// 32 x 8 -> 32 x 8: Function iters/ms 10%ile 50%ile 90%ile 10%ile 50%ile 90%ile
// (relative) (relative) (relative)
// ---------------------------------------------------------------------------------------------------------
// (Multiple<Zobrist, 8>) 31 31.5 31.8 1X 1X 1X
// (Multiple<MultiplyAddShift, 8>) 44.3 44.5 45.2 1.43X 1.41X 1.42X
// (Multiple<CRC, 8>) 121 123 123 3.9X 3.9X 3.88X
// (Multiple<MultiplyShift, 8>) 158 159 160 5.11X 5.05X 5.04X
// Zobrist256simple 16.5 16.5 16.6 0.533X 0.524X 0.522X
// Zobrist256gather 18.8 19.2 19.4 0.608X 0.608X 0.61X
// MultiplyAddShift256 151 154 154 4.88X 4.88X 4.84X
// MultiplyShift256 209 212 212 6.73X 6.71X 6.67X
// Rotate 32 bits right by 'shift'. This is _rotr in the intel instrinsics, but that isn't
// usable on Clang yet. Fortunately, both GCC and Clang can optimize this to use the 'ror'
// instruction.
template<int SHIFT>
inline uint32_t RotateRight(uint32_t x) {
static_assert(SHIFT > 0, "Only positive shifts are defined behavior and useful");
static_assert(
SHIFT < std::numeric_limits<decltype(x)>::digits, "This much shift is just 0");
return (x << (std::numeric_limits<decltype(x)>::digits - SHIFT)) | (x >> SHIFT);
}
// Make a random uint32_t, avoiding the absent high bit and the low-entropy low bits
// produced by rand().
uint32_t MakeRandU32() {
uint32_t result = (rand() >> 8) & 0xffff;
result <<= 16;
result |= (rand() >> 8) & 0xffff;
return result;
}
// Almost universal hashing, M. Dietzfelbinger, T. Hagerup, J. Katajainen, and M.
// Penttonen, "A reliable randomized algorithm for the closest-pair problem".
inline void MultiplyShift(uint32_t* x) {
static const uint32_t m = 0x61eaf8e9u;
*x = *x * m;
}
// Like MultiplyShift, but using SSE's 128-bit SIMD registers to do 4 at once.
//
// TODO: Add the Intel intrinsics used in this function and the other functions in this
// file to sse-util.h so that these functions can be inlined.
inline void MultiplyShift128(__m128i* x) __attribute__((__target__("sse4.1")));
inline void MultiplyShift128(__m128i* x) {
const __m128i m = _mm_set1_epi32(0x61eaf8e9);
_mm_storeu_si128(x, _mm_mullo_epi32(_mm_loadu_si128(x), m));
}
// Like MultiplyShift, but using AVX2's 256-bit SIMD registers to do 8 at once.
//
// Not inline, because it degrades the performance for unknown reasons.
void MultiplyShift256(__m256i* x) __attribute__((__target__("avx2")));
void MultiplyShift256(__m256i* x) {
const __m256i m = _mm256_set1_epi32(0x61eaf8e9);
_mm256_storeu_si256(x, _mm256_mullo_epi32(_mm256_loadu_si256(x), m));
}
// 2-independent hashing. M. Dietzfelbinger, "Universal hashing and k-wise independent
// random variables via integer arithmetic without primes"
inline void MultiplyAddShift(uint32_t* x) {
static const uint64_t m = 0xa1f1bd3e020b4be0ull, a = 0x86b0426193d86e66ull;
*x = (static_cast<uint64_t>(*x) * m + a) >> 32;
}
// Like MultiplyAddShift, but using SSE's 128-bit SIMD registers to do 4 at once.
inline void MultiplyAddShift128(__m128i* x) __attribute__((__target__("sse4.1")));
inline void MultiplyAddShift128(__m128i* x) {
const auto m = _mm_set1_epi64x(0xa1f1bd3e020b4be0ull),
mhi = _mm_set1_epi32(0xa1f1bd3e),
a = _mm_set1_epi64x(0x86b0426193d86e66ull);
auto input = _mm_loadu_si128(x);
auto prod32easy = _mm_mullo_epi32(input, mhi);
auto input_odds = _mm_srli_epi64(input, 32);
auto prod64_evens = _mm_mul_epu32(input, m),
prod64_odds = _mm_mul_epu32(input_odds, m);
prod64_evens = _mm_add_epi64(a, prod64_evens);
prod64_odds = _mm_add_epi64(a, prod64_odds);
auto prod32hard = _mm_unpackhi_epi32(prod64_evens, prod64_odds);
_mm_storeu_si128(x, _mm_add_epi32(prod32easy, prod32hard));
}
// Like MultiplyAddShift, but using AVX2's 256-bit SIMD registers to do 8 at once.
inline void MultiplyAddShift256(__m256i* x) __attribute__((__target__("avx2")));
inline void MultiplyAddShift256(__m256i* x) {
const __m256i m = _mm256_set1_epi64x(0xa1f1bd3e020b4be0ull),
mhi = _mm256_set1_epi32(0xa1f1bd3e),
a = _mm256_set1_epi64x(0x86b0426193d86e66ull);
__m256i input = _mm256_loadu_si256(x);
__m256i prod32easy = _mm256_mullo_epi32(input, mhi);
__m256i input_odds = _mm256_srli_epi64(input, 32);
__m256i prod64_evens = _mm256_mul_epu32(input, m),
prod64_odds = _mm256_mul_epu32(input_odds, m);
prod64_evens = _mm256_add_epi64(a, prod64_evens);
prod64_odds = _mm256_add_epi64(a, prod64_odds);
__m256i prod32hard = _mm256_unpackhi_epi32(prod64_evens, prod64_odds);
_mm256_storeu_si256(x, _mm256_add_epi32(prod32easy, prod32hard));
}
// From http://web.archive.org/web/20071223173210/http://www.concentric.net/~Ttwang/tech/inthash.htm:
inline void Jenkins1(int32_t* x) {
*x = ~*x + (*x << 15); // x = (x << 15) - x - 1;
*x = *x ^ RotateRight<12>(*x);
*x = *x + (*x << 2);
*x = *x ^ RotateRight<4>(*x);
*x = *x * 2057; // x = (x + (x << 3)) + (x << 11);
*x = *x ^ RotateRight<16>(*x);
}
// From http://web.archive.org/web/20071223173210/http://www.concentric.net/~Ttwang/tech/inthash.htm:
inline void Jenkins2(uint32_t* a) {
*a = (*a + 0x7ed55d16) + (*a << 12);
*a = (*a ^ 0xc761c23c) ^ (*a >> 19);
*a = (*a + 0x165667b1) + (*a << 5);
*a = (*a + 0xd3a2646c) ^ (*a << 9);
*a = (*a + 0xfd7046c5) + (*a << 3);
*a = (*a ^ 0xb55a4f09) ^ (*a >> 16);
}
// From http://web.archive.org/web/20071223173210/http://www.concentric.net/~Ttwang/tech/inthash.htm:
inline void MultRot(int32_t* key) {
static const int32_t c2 = 0x27d4eb2d; // a prime or an odd constant
*key = (*key ^ 61) ^ RotateRight<16>(*key);
*key = *key + (*key << 3);
*key = *key ^ RotateRight<4>(*key);
*key = *key * c2;
*key = *key ^ RotateRight<15>(*key);
}
inline void CRC(uint32_t* x) {
*x = SSE4_crc32_u32(*x, 0xab8ce2abu);
}
inline void FNV(uint32_t* key) {
*key = HashUtil::FnvHash64to32(key, sizeof(*key), HashUtil::FNV_SEED);
}
// Zobrist hashing, also known as tabulation hashing or simple tabulation hashing, is an
// old technique that has been recently analyzed and found to be very good for a number of
// applications. See "The Power of Simple Tabulation Hashing", by Mihai Patrascu and
// Mikkel Thorup.
uint32_t ZOBRIST_DATA[4][256];
inline void Zobrist(uint32_t* key) {
const uint8_t* key_chars = reinterpret_cast<const uint8_t*>(key);
*key = ZOBRIST_DATA[0][key_chars[0]] ^ ZOBRIST_DATA[1][key_chars[1]]
^ ZOBRIST_DATA[2][key_chars[2]] ^ ZOBRIST_DATA[3][key_chars[3]];
}
// Like Zobrist, but uses AVX2's "gather" primatives to hash 8 values at once.
inline void Zobrist256gather(__m256i* key) __attribute__((__target__("avx2")));
inline void Zobrist256gather(__m256i* key) {
const auto k = _mm256_loadu_si256(key);
const auto low_mask = _mm256_set1_epi32(0xff);
auto k0 = _mm256_and_si256(low_mask, k),
k1 = _mm256_and_si256(low_mask, _mm256_srli_epi32(k, 8)),
k2 = _mm256_and_si256(low_mask, _mm256_srli_epi32(k, 16)),
k3 = _mm256_and_si256(low_mask, _mm256_srli_epi32(k, 24));
k0 = _mm256_i32gather_epi32(reinterpret_cast<const int*>(ZOBRIST_DATA[0]), k0, 1);
k1 = _mm256_i32gather_epi32(reinterpret_cast<const int*>(ZOBRIST_DATA[1]), k1, 1);
k2 = _mm256_i32gather_epi32(reinterpret_cast<const int*>(ZOBRIST_DATA[2]), k2, 1);
k3 = _mm256_i32gather_epi32(reinterpret_cast<const int*>(ZOBRIST_DATA[3]), k3, 1);
auto k01 = _mm256_xor_si256(k0, k1), k23 = _mm256_xor_si256(k2, k3);
_mm256_storeu_si256(key, _mm256_xor_si256(k01, k23));
}
// Like Zobrist256gather, but only uses AVX2's SIMD xor, not its gather.
inline void Zobrist256simple(uint32_t (*key)[8]) __attribute__((__target__("avx2")));
inline void Zobrist256simple(uint32_t (*key)[8]) {
uint32_t row[4][8];
const uint8_t (*key_chars)[8][4] = reinterpret_cast<const uint8_t (*)[8][4]>(key);
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 8; ++j) {
row[i][j] = ZOBRIST_DATA[i][(*key_chars)[j][i]];
}
}
auto result0 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(row[0])),
result1 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(row[1])),
result2 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(row[2])),
result3 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(row[3]));
auto k01 = _mm256_xor_si256(result0, result1), k23 = _mm256_xor_si256(result2, result3);
_mm256_storeu_si256(reinterpret_cast<__m256i*>(*key), _mm256_xor_si256(k01, k23));
}
// Perform one hash function the given number of times. This can sometimes auto-vectorize.
//
// TODO: We could also test the costs of running on non-contiguous uint32_t's. For
// instance, ExprValuesCache.expr_values_array_ might have multiple values to hash per
// input row.
template <void (*F)(uint32_t*), size_t N>
inline void Multiple(uint32_t (*x)[N]) {
for (int i = 0; i < N; ++i) {
F((*x) + i);
}
}
// The size of the test data we run each hash function on:
static const size_t DATA_SIZE = 1 << 15;
template<typename T, void (*HASH)(T*)>
void Run(int batch_size, void* data) {
T* d = reinterpret_cast<T*>(data);
for (int i = 0; i < batch_size; ++i) {
for (int j = 0; j < ((sizeof(uint32_t)) * DATA_SIZE) / sizeof(T); ++j) {
HASH(&d[j]);
}
}
}
int main() {
CpuInfo::Init();
cout << endl
<< Benchmark::GetMachineInfo() << endl;
unique_ptr<uint32_t[]> ud(new uint32_t[DATA_SIZE]);
for (size_t i = 0; i < (DATA_SIZE); ++i) {
ud[i] = MakeRandU32();
}
for (size_t i = 0; i < 4; ++i) {
for (size_t j = 0; j < 256; ++j) {
ZOBRIST_DATA[i][j] = MakeRandU32();
}
}
Benchmark suite32("32 -> 32");
#define BENCH(T,x) AddBenchmark(#x, Run<T, x>, ud.get())
suite32.BENCH(uint32_t, FNV);
suite32.BENCH(uint32_t, Zobrist);
suite32.BENCH(int32_t, MultRot);
suite32.BENCH(uint32_t, MultiplyAddShift);
suite32.BENCH(int32_t, Jenkins1);
suite32.BENCH(uint32_t, Jenkins2);
if (CpuInfo::IsSupported(CpuInfo::SSE4_2)) suite32.BENCH(uint32_t, CRC);
suite32.BENCH(uint32_t, MultiplyShift);
cout << suite32.Measure() << endl;
Benchmark suite32x4("32 x 4 -> 32 x 4");
suite32x4.BENCH(uint32_t[4], (Multiple<Zobrist, 4>));
suite32x4.BENCH(uint32_t[4], (Multiple<MultiplyAddShift, 4>));
if (CpuInfo::IsSupported(CpuInfo::SSE4_2)) {
suite32x4.BENCH(uint32_t[4], (Multiple<CRC, 4>));
}
suite32x4.BENCH(uint32_t[4], (Multiple<MultiplyShift, 4>));
if (CpuInfo::IsSupported(CpuInfo::SSE4_1)) {
suite32x4.BENCH(__m128i, MultiplyAddShift128);
suite32x4.BENCH(__m128i, MultiplyShift128);
}
cout << suite32x4.Measure() << endl;
Benchmark suite32x8("32 x 8 -> 32 x 8");
suite32x8.BENCH(uint32_t[8], (Multiple<Zobrist, 8>));
suite32x8.BENCH(uint32_t[8], (Multiple<MultiplyAddShift, 8>));
suite32x8.BENCH(uint32_t[8], (Multiple<CRC, 8>));
suite32x8.BENCH(uint32_t[8], (Multiple<MultiplyShift, 8>));
if (CpuInfo::IsSupported(CpuInfo::AVX2)) {
suite32x8.BENCH(uint32_t[8], Zobrist256simple);
suite32x8.BENCH(__m256i, Zobrist256gather);
suite32x8.BENCH(__m256i, MultiplyAddShift256);
suite32x8.BENCH(__m256i, MultiplyShift256);
}
cout << suite32x8.Measure() << endl;
#undef BENCH
}