Enable vector byte shuffle optimizations on ARM NEON

The SSSE3 intrinsics we use have their direct analogues in NEON, so making this optimization portable requires a very thin translation layer.

PiperOrigin-RevId: 381280165
This commit is contained in:
atdt 2021-06-24 17:09:34 +00:00 committed by Victor Costan
parent b638ebe5d9
commit b3fb0b5b4b
2 changed files with 99 additions and 59 deletions

View File

@ -36,6 +36,56 @@
namespace snappy {
namespace internal {
#if SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
#if SNAPPY_HAVE_SSSE3
using V128 = __m128i;
#else
using V128 = uint8x16_t;
#endif
// Load 128 bits of integer data. `src` must be 16-byte aligned.
inline V128 V128_Load(const V128* src);
// Load 128 bits of integer data. `src` does not need to be aligned.
inline V128 V128_LoadU(const V128* src);
// Store 128 bits of integer data. `dst` does not need to be aligned.
inline void V128_StoreU(V128* dst, V128 val);
// Shuffle packed 8-bit integers using a shuffle mask.
// Each packed integer in the shuffle mask must be in [0,16).
inline V128 V128_Shuffle(V128 input, V128 shuffle_mask);
#if SNAPPY_HAVE_SSSE3
inline V128 V128_Load(const V128* src) { return _mm_load_si128(src); }
inline V128 V128_LoadU(const V128* src) { return _mm_loadu_si128(src); }
inline void V128_StoreU(V128* dst, V128 val) { _mm_storeu_si128(dst, val); }
inline V128 V128_Shuffle(V128 input, V128 shuffle_mask) {
return _mm_shuffle_epi8(input, shuffle_mask);
}
#else
inline V128 V128_Load(const V128* src) {
return vld1q_u8(reinterpret_cast<const uint8_t*>(src));
}
inline V128 V128_LoadU(const V128* src) {
return vld1q_u8(reinterpret_cast<const uint8_t*>(src));
}
inline void V128_StoreU(V128* dst, V128 val) {
vst1q_u8(reinterpret_cast<uint8_t*>(dst), val);
}
inline V128 V128_Shuffle(V128 input, V128 shuffle_mask) {
assert(vminvq_u8(shuffle_mask) >= 0 && vmaxvq_u8(shuffle_mask) <= 15);
return vqtbl1q_u8(input, shuffle_mask);
}
#endif
#endif // SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
// Working memory performs a single allocation to hold all scratch space
// required for compression.
class WorkingMemory {

108
snappy.cc
View File

@ -30,17 +30,6 @@
#include "snappy-sinksource.h"
#include "snappy.h"
#if !defined(SNAPPY_HAVE_SSSE3)
// __SSSE3__ is defined by GCC and Clang. Visual Studio doesn't target SIMD
// support between SSE2 and AVX (so SSSE3 instructions require AVX support), and
// defines __AVX__ when AVX support is available.
#if defined(__SSSE3__) || defined(__AVX__)
#define SNAPPY_HAVE_SSSE3 1
#else
#define SNAPPY_HAVE_SSSE3 0
#endif
#endif // !defined(SNAPPY_HAVE_SSSE3)
#if !defined(SNAPPY_HAVE_BMI2)
// __BMI2__ is defined by GCC and Clang. Visual Studio doesn't target BMI2
// specifically, but it does define __AVX2__ when AVX2 support is available.
@ -56,12 +45,6 @@
#endif
#endif // !defined(SNAPPY_HAVE_BMI2)
#if SNAPPY_HAVE_SSSE3
// Please do not replace with <x86intrin.h>. or with headers that assume more
// advanced SSE versions without checking with all the OWNERS.
#include <tmmintrin.h>
#endif
#if SNAPPY_HAVE_BMI2
// Please do not replace with <x86intrin.h>. or with headers that assume more
// advanced SSE versions without checking with all the OWNERS.
@ -91,6 +74,13 @@ using internal::COPY_2_BYTE_OFFSET;
using internal::COPY_4_BYTE_OFFSET;
using internal::kMaximumTagLength;
using internal::LITERAL;
#if SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
using internal::V128;
using internal::V128_Load;
using internal::V128_LoadU;
using internal::V128_Shuffle;
using internal::V128_StoreU;
#endif
// We translate the information encoded in a tag through a lookup table to a
// format that requires fewer instructions to decode. Effectively we store
@ -228,7 +218,7 @@ inline char* IncrementalCopySlow(const char* src, char* op,
return op_limit;
}
#if SNAPPY_HAVE_SSSE3
#if SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
// Computes the bytes for shuffle control mask (please read comments on
// 'pattern_generation_masks' as well) for the given index_offset and
@ -248,19 +238,19 @@ inline constexpr std::array<char, sizeof...(indexes)> MakePatternMaskBytes(
// Computes the shuffle control mask bytes array for given pattern-sizes and
// returns an array.
template <size_t... pattern_sizes_minus_one>
inline constexpr std::array<std::array<char, sizeof(__m128i)>,
inline constexpr std::array<std::array<char, sizeof(V128)>,
sizeof...(pattern_sizes_minus_one)>
MakePatternMaskBytesTable(int index_offset,
index_sequence<pattern_sizes_minus_one...>) {
return {MakePatternMaskBytes(
index_offset, pattern_sizes_minus_one + 1,
make_index_sequence</*indexes=*/sizeof(__m128i)>())...};
return {
MakePatternMaskBytes(index_offset, pattern_sizes_minus_one + 1,
make_index_sequence</*indexes=*/sizeof(V128)>())...};
}
// This is an array of shuffle control masks that can be used as the source
// operand for PSHUFB to permute the contents of the destination XMM register
// into a repeating byte pattern.
alignas(16) constexpr std::array<std::array<char, sizeof(__m128i)>,
alignas(16) constexpr std::array<std::array<char, sizeof(V128)>,
16> pattern_generation_masks =
MakePatternMaskBytesTable(
/*index_offset=*/0,
@ -271,40 +261,40 @@ alignas(16) constexpr std::array<std::array<char, sizeof(__m128i)>,
// Basically, pattern_reshuffle_masks is a continuation of
// pattern_generation_masks. It follows that, pattern_reshuffle_masks is same as
// pattern_generation_masks for offsets 1, 2, 4, 8 and 16.
alignas(16) constexpr std::array<std::array<char, sizeof(__m128i)>,
alignas(16) constexpr std::array<std::array<char, sizeof(V128)>,
16> pattern_reshuffle_masks =
MakePatternMaskBytesTable(
/*index_offset=*/16,
/*pattern_sizes_minus_one=*/make_index_sequence<16>());
SNAPPY_ATTRIBUTE_ALWAYS_INLINE
static inline __m128i LoadPattern(const char* src, const size_t pattern_size) {
__m128i generation_mask = _mm_load_si128(reinterpret_cast<const __m128i*>(
static inline V128 LoadPattern(const char* src, const size_t pattern_size) {
V128 generation_mask = V128_Load(reinterpret_cast<const V128*>(
pattern_generation_masks[pattern_size - 1].data()));
// Uninitialized bytes are masked out by the shuffle mask.
// TODO: remove annotation and macro defs once MSan is fixed.
SNAPPY_ANNOTATE_MEMORY_IS_INITIALIZED(src + pattern_size, 16 - pattern_size);
return _mm_shuffle_epi8(
_mm_loadu_si128(reinterpret_cast<const __m128i*>(src)), generation_mask);
return V128_Shuffle(V128_LoadU(reinterpret_cast<const V128*>(src)),
generation_mask);
}
SNAPPY_ATTRIBUTE_ALWAYS_INLINE
static inline std::pair<__m128i /* pattern */, __m128i /* reshuffle_mask */>
static inline std::pair<V128 /* pattern */, V128 /* reshuffle_mask */>
LoadPatternAndReshuffleMask(const char* src, const size_t pattern_size) {
__m128i pattern = LoadPattern(src, pattern_size);
V128 pattern = LoadPattern(src, pattern_size);
// This mask will generate the next 16 bytes in-place. Doing so enables us to
// write data by at most 4 _mm_storeu_si128.
// write data by at most 4 V128_StoreU.
//
// For example, suppose pattern is: abcdefabcdefabcd
// Shuffling with this mask will generate: efabcdefabcdefab
// Shuffling again will generate: cdefabcdefabcdef
__m128i reshuffle_mask = _mm_load_si128(reinterpret_cast<const __m128i*>(
V128 reshuffle_mask = V128_Load(reinterpret_cast<const V128*>(
pattern_reshuffle_masks[pattern_size - 1].data()));
return {pattern, reshuffle_mask};
}
#endif // SNAPPY_HAVE_SSSE3
#endif // SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
// Fallback for when we need to copy while extending the pattern, for example
// copying 10 bytes from 3 positions back abc -> abcabcabcabca.
@ -312,7 +302,7 @@ LoadPatternAndReshuffleMask(const char* src, const size_t pattern_size) {
// REQUIRES: [dst - offset, dst + 64) is a valid address range.
SNAPPY_ATTRIBUTE_ALWAYS_INLINE
static inline bool Copy64BytesWithPatternExtension(char* dst, size_t offset) {
#if SNAPPY_HAVE_SSSE3
#if SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
if (SNAPPY_PREDICT_TRUE(offset <= 16)) {
switch (offset) {
case 0:
@ -325,20 +315,20 @@ static inline bool Copy64BytesWithPatternExtension(char* dst, size_t offset) {
case 4:
case 8:
case 16: {
__m128i pattern = LoadPattern(dst - offset, offset);
V128 pattern = LoadPattern(dst - offset, offset);
for (int i = 0; i < 4; i++) {
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 16 * i), pattern);
V128_StoreU(reinterpret_cast<V128*>(dst + 16 * i), pattern);
}
return true;
}
default: {
auto pattern_and_reshuffle_mask =
LoadPatternAndReshuffleMask(dst - offset, offset);
__m128i pattern = pattern_and_reshuffle_mask.first;
__m128i reshuffle_mask = pattern_and_reshuffle_mask.second;
V128 pattern = pattern_and_reshuffle_mask.first;
V128 reshuffle_mask = pattern_and_reshuffle_mask.second;
for (int i = 0; i < 4; i++) {
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 16 * i), pattern);
pattern = _mm_shuffle_epi8(pattern, reshuffle_mask);
V128_StoreU(reinterpret_cast<V128*>(dst + 16 * i), pattern);
pattern = V128_Shuffle(pattern, reshuffle_mask);
}
return true;
}
@ -361,7 +351,7 @@ static inline bool Copy64BytesWithPatternExtension(char* dst, size_t offset) {
}
return true;
}
#endif // SNAPPY_HAVE_SSSE3
#endif // SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
// Very rare.
for (int i = 0; i < 4; i++) {
@ -375,7 +365,7 @@ static inline bool Copy64BytesWithPatternExtension(char* dst, size_t offset) {
// region of the buffer.
inline char* IncrementalCopy(const char* src, char* op, char* const op_limit,
char* const buf_limit) {
#if SNAPPY_HAVE_SSSE3
#if SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
constexpr int big_pattern_size_lower_bound = 16;
#else
constexpr int big_pattern_size_lower_bound = 8;
@ -425,14 +415,14 @@ inline char* IncrementalCopy(const char* src, char* op, char* const op_limit,
// Handle the uncommon case where pattern is less than 16 (or 8 in non-SSE)
// bytes.
if (pattern_size < big_pattern_size_lower_bound) {
#if SNAPPY_HAVE_SSSE3
#if SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
// Load the first eight bytes into an 128-bit XMM register, then use PSHUFB
// to permute the register's contents in-place into a repeating sequence of
// the first "pattern_size" bytes.
// For example, suppose:
// src == "abc"
// op == op + 3
// After _mm_shuffle_epi8(), "pattern" will have five copies of "abc"
// After V128_Shuffle(), "pattern" will have five copies of "abc"
// followed by one byte of slop: abcabcabcabcabca.
//
// The non-SSE fallback implementation suffers from store-forwarding stalls
@ -444,26 +434,26 @@ inline char* IncrementalCopy(const char* src, char* op, char* const op_limit,
if (SNAPPY_PREDICT_TRUE(op_limit <= buf_limit - 15)) {
auto pattern_and_reshuffle_mask =
LoadPatternAndReshuffleMask(src, pattern_size);
__m128i pattern = pattern_and_reshuffle_mask.first;
__m128i reshuffle_mask = pattern_and_reshuffle_mask.second;
V128 pattern = pattern_and_reshuffle_mask.first;
V128 reshuffle_mask = pattern_and_reshuffle_mask.second;
// There is at least one, and at most four 16-byte blocks. Writing four
// conditionals instead of a loop allows FDO to layout the code with
// respect to the actual probabilities of each length.
// TODO: Replace with loop with trip count hint.
_mm_storeu_si128(reinterpret_cast<__m128i*>(op), pattern);
V128_StoreU(reinterpret_cast<V128*>(op), pattern);
if (op + 16 < op_limit) {
pattern = _mm_shuffle_epi8(pattern, reshuffle_mask);
_mm_storeu_si128(reinterpret_cast<__m128i*>(op + 16), pattern);
pattern = V128_Shuffle(pattern, reshuffle_mask);
V128_StoreU(reinterpret_cast<V128*>(op + 16), pattern);
}
if (op + 32 < op_limit) {
pattern = _mm_shuffle_epi8(pattern, reshuffle_mask);
_mm_storeu_si128(reinterpret_cast<__m128i*>(op + 32), pattern);
pattern = V128_Shuffle(pattern, reshuffle_mask);
V128_StoreU(reinterpret_cast<V128*>(op + 32), pattern);
}
if (op + 48 < op_limit) {
pattern = _mm_shuffle_epi8(pattern, reshuffle_mask);
_mm_storeu_si128(reinterpret_cast<__m128i*>(op + 48), pattern);
pattern = V128_Shuffle(pattern, reshuffle_mask);
V128_StoreU(reinterpret_cast<V128*>(op + 48), pattern);
}
return op_limit;
}
@ -471,8 +461,8 @@ inline char* IncrementalCopy(const char* src, char* op, char* const op_limit,
if (SNAPPY_PREDICT_TRUE(op < op_end)) {
auto pattern_and_reshuffle_mask =
LoadPatternAndReshuffleMask(src, pattern_size);
__m128i pattern = pattern_and_reshuffle_mask.first;
__m128i reshuffle_mask = pattern_and_reshuffle_mask.second;
V128 pattern = pattern_and_reshuffle_mask.first;
V128 reshuffle_mask = pattern_and_reshuffle_mask.second;
// This code path is relatively cold however so we save code size
// by avoiding unrolling and vectorizing.
@ -483,13 +473,13 @@ inline char* IncrementalCopy(const char* src, char* op, char* const op_limit,
#pragma clang loop unroll(disable)
#endif
do {
_mm_storeu_si128(reinterpret_cast<__m128i*>(op), pattern);
pattern = _mm_shuffle_epi8(pattern, reshuffle_mask);
V128_StoreU(reinterpret_cast<V128*>(op), pattern);
pattern = V128_Shuffle(pattern, reshuffle_mask);
op += 16;
} while (SNAPPY_PREDICT_TRUE(op < op_end));
}
return IncrementalCopySlow(op - pattern_size, op, op_limit);
#else // !SNAPPY_HAVE_SSSE3
#else // !SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
// If plenty of buffer space remains, expand the pattern to at least 8
// bytes. The way the following loop is written, we need 8 bytes of buffer
// space if pattern_size >= 4, 11 bytes if pattern_size is 1 or 3, and 10
@ -506,7 +496,7 @@ inline char* IncrementalCopy(const char* src, char* op, char* const op_limit,
} else {
return IncrementalCopySlow(src, op, op_limit);
}
#endif // SNAPPY_HAVE_SSSE3
#endif // SNAPPY_HAVE_VECTOR_BYTE_SHUFFLE
}
assert(pattern_size >= big_pattern_size_lower_bound);
constexpr bool use_16bytes_chunk = big_pattern_size_lower_bound == 16;