Polyfill GroupSse2Impl on non-x86 platforms

Bug: v8:12518
Change-Id: Ie22303416749affc0629d60fbed6f9dc4288b09d
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3494443
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Commit-Queue: David Benjamin <davidben@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79834}
This commit is contained in:
David Benjamin 2022-04-01 11:07:15 -04:00 committed by V8 LUCI CQ
parent 1ac9280d50
commit 6cf7330a61
3 changed files with 217 additions and 44 deletions

View File

@ -17,51 +17,50 @@
#ifndef V8_OBJECTS_SWISS_HASH_TABLE_HELPERS_H_
#define V8_OBJECTS_SWISS_HASH_TABLE_HELPERS_H_
// The following #defines are taken from Abseil's have_sse.h (but renamed). They
// are only defined within this file. However, we also take cross platform
// snapshot creation into account, by only using SSE if the target supports it,
// too. The SSE implementation uses a group width of 16, whereas the non-SSE
// version uses 8. We therefore have to avoid building a snapshot that contains
// Swiss Tables with one group size and use it in code that excepts a different
// group size.
#ifndef SWISS_TABLE_HAVE_SSE2
#if (defined(__SSE2__) || \
(defined(_MSC_VER) && \
(defined(_M_X64) || (defined(_M_IX86) && _M_IX86_FP >= 2)))) && \
(defined(V8_TARGET_ARCH_IA32) || defined(V8_TARGET_ARCH_X64))
#define SWISS_TABLE_HAVE_SSE2 1
// The following #defines are taken from Abseil's have_sse.h (but renamed).
#ifndef V8_SWISS_TABLE_HAVE_SSE2_HOST
#if (defined(__SSE2__) || \
(defined(_MSC_VER) && \
(defined(_M_X64) || (defined(_M_IX86) && _M_IX86_FP >= 2))))
#define V8_SWISS_TABLE_HAVE_SSE2_HOST 1
#else
#define SWISS_TABLE_HAVE_SSE2 0
#if defined(V8_TARGET_ARCH_IA32) || defined(V8_TARGET_ARCH_X64)
// TODO(v8:11388) Currently, building on a non-SSE platform for a SSE target
// means that we cannot use the (more performant) SSE implementations of Swiss
// Tables, even if the target would support it, just because the host doesn't.
// This is due to the difference in group sizes (see comment at the beginning of
// the file). We can solve this by implementating a new non-SSE Group that
// behaves like GroupSse2Impl (and uses group size 16) in the future.
#warning "You should avoid building on a non-SSE platform for a SSE target!"
#endif
#define V8_SWISS_TABLE_HAVE_SSE2_HOST 0
#endif
#endif
#ifndef SWISS_TABLE_HAVE_SSSE3
#if defined(__SSSE3__) && \
(defined(V8_TARGET_ARCH_IA32) || defined(V8_TARGET_ARCH_X64))
#define SWISS_TABLE_HAVE_SSSE3 1
#ifndef V8_SWISS_TABLE_HAVE_SSSE3_HOST
#if defined(__SSSE3__)
#define V8_SWISS_TABLE_HAVE_SSSE3_HOST 1
#else
#define SWISS_TABLE_HAVE_SSSE3 0
#define V8_SWISS_TABLE_HAVE_SSSE3_HOST 0
#endif
#endif
#if SWISS_TABLE_HAVE_SSSE3 && !SWISS_TABLE_HAVE_SSE2
#if V8_SWISS_TABLE_HAVE_SSSE3_HOST && !V8_SWISS_TABLE_HAVE_SSE2_HOST
#error "Bad configuration!"
#endif
#if SWISS_TABLE_HAVE_SSE2
// Unlike Abseil, we cannot select SSE purely by host capabilities. When
// creating a snapshot, the group width must be compatible. The SSE
// implementation uses a group width of 16, whereas the non-SSE version uses 8.
// Thus we select the group size based on target capabilities and, if the host
// does not match, select a polyfill implementation. This means, in supported
// cross-compiling configurations, we must be able to determine matching target
// capabilities from the host.
#ifndef V8_SWISS_TABLE_HAVE_SSE2_TARGET
#if V8_TARGET_ARCH_IA32 || V8_TARGET_ARCH_X64
// x64 always has SSE2, and ia32 without SSE2 is not supported by V8.
#define V8_SWISS_TABLE_HAVE_SSE2_TARGET 1
#else
#define V8_SWISS_TABLE_HAVE_SSE2_TARGET 0
#endif
#endif
#if V8_SWISS_TABLE_HAVE_SSE2_HOST
#include <emmintrin.h>
#endif
#if SWISS_TABLE_HAVE_SSSE3
#if V8_SWISS_TABLE_HAVE_SSSE3_HOST
#include <tmmintrin.h>
#endif
@ -126,9 +125,9 @@ class BitMask {
public:
// These are useful for unit tests (gunit).
// using value_type = int;
// using iterator = BitMask;
// using const_iterator = BitMask;
using value_type = int;
using iterator = BitMask;
using const_iterator = BitMask;
explicit BitMask(T mask) : mask_(mask) {}
BitMask& operator++() {
@ -219,7 +218,7 @@ inline static swiss_table::ctrl_t H2(uint32_t hash) {
return hash & ((1 << kH2Bits) - 1);
}
#if SWISS_TABLE_HAVE_SSE2
#if V8_SWISS_TABLE_HAVE_SSE2_HOST
// https://github.com/abseil/abseil-cpp/issues/209
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=87853
// _mm_cmpgt_epi8 is broken under GCC with -funsigned-char
@ -252,7 +251,7 @@ struct GroupSse2Impl {
// Returns a bitmask representing the positions of empty slots.
BitMask<uint32_t, kWidth> MatchEmpty() const {
#if SWISS_TABLE_HAVE_SSSE3
#if V8_SWISS_TABLE_HAVE_SSSE3_HOST
// This only works because kEmpty is -128.
return BitMask<uint32_t, kWidth>(
_mm_movemask_epi8(_mm_sign_epi8(ctrl, ctrl)));
@ -278,7 +277,7 @@ struct GroupSse2Impl {
void ConvertSpecialToEmptyAndFullToDeleted(ctrl_t* dst) const {
auto msbs = _mm_set1_epi8(static_cast<char>(-128));
auto x126 = _mm_set1_epi8(126);
#if SWISS_TABLE_HAVE_SSSE3
#if V8_SWISS_TABLE_HAVE_SSSE3_HOST
auto res = _mm_or_si128(_mm_shuffle_epi8(x126, ctrl), msbs);
#else
auto zero = _mm_setzero_si128();
@ -290,7 +289,64 @@ struct GroupSse2Impl {
__m128i ctrl;
};
#endif // SWISS_TABLE_HAVE_SSE2
#endif // V8_SWISS_TABLE_HAVE_SSE2_HOST
// A portable, inefficient version of GroupSse2Impl. This exists so SSE2-less
// hosts can generate snapshots for SSE2-capable targets.
struct GroupSse2Polyfill {
static constexpr size_t kWidth = 16; // the number of slots per group
explicit GroupSse2Polyfill(const ctrl_t* pos) { memcpy(ctrl_, pos, kWidth); }
// Returns a bitmask representing the positions of slots that match |hash|.
BitMask<uint32_t, kWidth> Match(h2_t hash) const {
uint32_t mask = 0;
for (size_t i = 0; i < kWidth; i++) {
if (static_cast<h2_t>(ctrl_[i]) == hash) {
mask |= 1u << i;
}
}
return BitMask<uint32_t, kWidth>(mask);
}
// Returns a bitmask representing the positions of empty slots.
BitMask<uint32_t, kWidth> MatchEmpty() const {
return Match(static_cast<h2_t>(kEmpty));
}
// Returns a bitmask representing the positions of empty or deleted slots.
BitMask<uint32_t, kWidth> MatchEmptyOrDeleted() const {
return BitMask<uint32_t, kWidth>(MatchEmptyOrDeletedMask());
}
// Returns the number of trailing empty or deleted elements in the group.
uint32_t CountLeadingEmptyOrDeleted() const {
return base::bits::CountTrailingZerosNonZero(MatchEmptyOrDeletedMask() + 1);
}
void ConvertSpecialToEmptyAndFullToDeleted(ctrl_t* dst) const {
for (size_t i = 0; i < kWidth; i++) {
if (ctrl_[i] < 0) {
dst[i] = kEmpty;
} else {
dst[i] = kDeleted;
}
}
}
private:
uint32_t MatchEmptyOrDeletedMask() const {
uint32_t mask = 0;
for (size_t i = 0; i < kWidth; i++) {
if (ctrl_[i] < kSentinel) {
mask |= 1u << i;
}
}
return mask;
}
ctrl_t ctrl_[kWidth];
};
struct GroupPortableImpl {
static constexpr size_t kWidth = 8; // the number of slots per group
@ -366,16 +422,23 @@ struct GroupPortableImpl {
// backend should only use SSE2 when compiling the SIMD version of
// SwissNameDictionary into the builtin.
using Group = GroupPortableImpl;
#else
#if SWISS_TABLE_HAVE_SSE2
#elif V8_SWISS_TABLE_HAVE_SSE2_TARGET
// Use a matching group size between host and target.
#if V8_SWISS_TABLE_HAVE_SSE2_HOST
using Group = GroupSse2Impl;
#else
#if V8_HOST_ARCH_IA32 || V8_HOST_ARCH_X64
// If we do not detect SSE2 when building for the ia32/x64 target, the
// V8_SWISS_TABLE_HAVE_SSE2_TARGET logic will incorrectly cause the final output
// to use the inefficient polyfill implementation. Detect this case and warn if
// it happens.
#warning "Did not detect required SSE2 support on ia32/x64."
#endif
using Group = GroupSse2Polyfill;
#endif
#else
using Group = GroupPortableImpl;
#endif
#endif
#undef SWISS_TABLE_HAVE_SSE2
#undef SWISS_TABLE_HAVE_SSE3
} // namespace swiss_table
} // namespace internal

View File

@ -368,6 +368,7 @@ v8_source_set("unittests_sources") {
"numbers/conversions-unittest.cc",
"objects/object-unittest.cc",
"objects/osr-optimized-code-cache-unittest.cc",
"objects/swiss-hash-table-helpers-unittest.cc",
"objects/value-serializer-unittest.cc",
"objects/weakarraylist-unittest.cc",
"parser/ast-value-unittest.cc",

View File

@ -0,0 +1,109 @@
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/objects/swiss-hash-table-helpers.h"
#include <vector>
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::ElementsAre;
namespace v8 {
namespace internal {
namespace swiss_table {
template <typename T>
class SwissTableGroupTest : public testing::Test {};
using GroupTypes = testing::Types<
#if V8_SWISS_TABLE_HAVE_SSE2_HOST
GroupSse2Impl,
#endif
GroupSse2Polyfill, GroupPortableImpl>;
TYPED_TEST_SUITE(SwissTableGroupTest, GroupTypes);
// Tests imported from Abseil's raw_hash_set_test.cc, modified to be
// parameterized.
TYPED_TEST(SwissTableGroupTest, EmptyGroup) {
const ctrl_t kEmptyGroup[16] = {
kSentinel, kEmpty, kEmpty, kEmpty, kEmpty, kEmpty, kEmpty, kEmpty,
kEmpty, kEmpty, kEmpty, kEmpty, kEmpty, kEmpty, kEmpty, kEmpty,
};
for (h2_t h = 0; h != 128; ++h) EXPECT_FALSE(TypeParam{kEmptyGroup}.Match(h));
}
TYPED_TEST(SwissTableGroupTest, Match) {
if (TypeParam::kWidth == 16) {
ctrl_t group[] = {kEmpty, 1, kDeleted, 3, kEmpty, 5, kSentinel, 7,
7, 5, 3, 1, 1, 1, 1, 1};
EXPECT_THAT(TypeParam{group}.Match(0), ElementsAre());
EXPECT_THAT(TypeParam{group}.Match(1), ElementsAre(1, 11, 12, 13, 14, 15));
EXPECT_THAT(TypeParam{group}.Match(3), ElementsAre(3, 10));
EXPECT_THAT(TypeParam{group}.Match(5), ElementsAre(5, 9));
EXPECT_THAT(TypeParam{group}.Match(7), ElementsAre(7, 8));
} else if (TypeParam::kWidth == 8) {
ctrl_t group[] = {kEmpty, 1, 2, kDeleted, 2, 1, kSentinel, 1};
EXPECT_THAT(TypeParam{group}.Match(0), ElementsAre());
EXPECT_THAT(TypeParam{group}.Match(1), ElementsAre(1, 5, 7));
EXPECT_THAT(TypeParam{group}.Match(2), ElementsAre(2, 4));
} else {
FAIL() << "No test coverage for kWidth==" << TypeParam::kWidth;
}
}
TYPED_TEST(SwissTableGroupTest, MatchEmpty) {
if (TypeParam::kWidth == 16) {
ctrl_t group[] = {kEmpty, 1, kDeleted, 3, kEmpty, 5, kSentinel, 7,
7, 5, 3, 1, 1, 1, 1, 1};
EXPECT_THAT(TypeParam{group}.MatchEmpty(), ElementsAre(0, 4));
} else if (TypeParam::kWidth == 8) {
ctrl_t group[] = {kEmpty, 1, 2, kDeleted, 2, 1, kSentinel, 1};
EXPECT_THAT(TypeParam{group}.MatchEmpty(), ElementsAre(0));
} else {
FAIL() << "No test coverage for kWidth==" << TypeParam::kWidth;
}
}
TYPED_TEST(SwissTableGroupTest, MatchEmptyOrDeleted) {
if (TypeParam::kWidth == 16) {
ctrl_t group[] = {kEmpty, 1, kDeleted, 3, kEmpty, 5, kSentinel, 7,
7, 5, 3, 1, 1, 1, 1, 1};
EXPECT_THAT(TypeParam{group}.MatchEmptyOrDeleted(), ElementsAre(0, 2, 4));
} else if (TypeParam::kWidth == 8) {
ctrl_t group[] = {kEmpty, 1, 2, kDeleted, 2, 1, kSentinel, 1};
EXPECT_THAT(TypeParam{group}.MatchEmptyOrDeleted(), ElementsAre(0, 3));
} else {
FAIL() << "No test coverage for kWidth==" << TypeParam::kWidth;
}
}
TYPED_TEST(SwissTableGroupTest, CountLeadingEmptyOrDeleted) {
const std::vector<ctrl_t> empty_examples = {kEmpty, kDeleted};
const std::vector<ctrl_t> full_examples = {0, 1, 2, 3, 5, 9, 127, kSentinel};
for (ctrl_t empty : empty_examples) {
std::vector<ctrl_t> e(TypeParam::kWidth, empty);
EXPECT_EQ(TypeParam::kWidth,
TypeParam{e.data()}.CountLeadingEmptyOrDeleted());
for (ctrl_t full : full_examples) {
for (size_t i = 0; i != TypeParam::kWidth; ++i) {
std::vector<ctrl_t> f(TypeParam::kWidth, empty);
f[i] = full;
EXPECT_EQ(i, TypeParam{f.data()}.CountLeadingEmptyOrDeleted());
}
std::vector<ctrl_t> f(TypeParam::kWidth, empty);
f[TypeParam::kWidth * 2 / 3] = full;
f[TypeParam::kWidth / 2] = full;
EXPECT_EQ(TypeParam::kWidth / 2,
TypeParam{f.data()}.CountLeadingEmptyOrDeleted());
}
}
}
} // namespace swiss_table
} // namespace internal
} // namespace v8