diff --git a/.github/workflows/test_windows.yml b/.github/workflows/test_windows.yml new file mode 100644 index 00000000..6b134ceb --- /dev/null +++ b/.github/workflows/test_windows.yml @@ -0,0 +1,28 @@ +name: CI / Test (Windows) + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + test-msvc: + name: Visual Studio 2022 / x64 / AVX2 + runs-on: windows-2022 + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v4 + - name: Configure + run: cmake -S . -B build -A x64 -DENABLE_ASAN=OFF + - name: Build + run: cmake --build build --config Release --target unittest --parallel + - name: Test + run: ctest --test-dir build -C Release --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 69bd55c3..adb37b9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.11) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project( sonic_cpp @@ -15,6 +16,7 @@ option(ENABLE_RISCV_ZBB "Enable RISC-V Zbb extension for bit manipulation optimi set(CMAKE_CXX_EXTENSIONS OFF) if(BUILD_UNITTEST) + enable_testing() add_subdirectory(tests) endif() diff --git a/README.md b/README.md index 3d2bb960..bf485c70 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,27 @@ A fast JSON serializing & deserializing library, accelerated by SIMD. [![clang-format Check](https://github.com/bytedance/sonic-cpp/actions/workflows/clang-format-check.yml/badge.svg)](https://github.com/bytedance/sonic-cpp/actions/workflows/clang-format-check.yml) [![Test](https://github.com/bytedance/sonic-cpp/actions/workflows/test_x86.yml/badge.svg)](https://github.com/bytedance/sonic-cpp/actions/workflows/test_x86.yml) +[![Windows](https://github.com/bytedance/sonic-cpp/actions/workflows/test_windows.yml/badge.svg)](https://github.com/bytedance/sonic-cpp/actions/workflows/test_windows.yml) [![codecov](https://codecov.io/gh/bytedance/sonic-cpp/branch/master/graph/badge.svg)](https://codecov.io/gh/bytedance/sonic-cpp) ## Requirement -- c++11 or above -- x86 platform with AVX2 instruction -- GCC or LLVM compiler (not support MSVC compiler now) -- Linux OS +- C++17 or above +- A supported SIMD target (x86-64 with AVX2, AArch64, or RV64) +- GCC, LLVM/Clang, or Microsoft Visual C++ from Visual Studio 2022 +- Linux, macOS, or 64-bit Windows + +### Visual Studio 2022 + +The CMake build selects `/std:c++17` and `/arch:AVX2` for MSVC automatically: + +```powershell +cmake -S . -B build -A x64 -DENABLE_ASAN=OFF +cmake --build build --config Release --target unittest --parallel +ctest --test-dir build -C Release --output-on-failure +``` + +When consuming the header-only library from a Visual Studio project directly, +enable C++17 and **Advanced Vector Extensions 2** (`/arch:AVX2`). ## Features - Complete APIs for JSON value manipulation diff --git a/cmake/set_arch_flags.cmake b/cmake/set_arch_flags.cmake index 8c679989..8df9d3ed 100644 --- a/cmake/set_arch_flags.cmake +++ b/cmake/set_arch_flags.cmake @@ -1,8 +1,14 @@ function(set_arch_flags target arch) message(STATUS "Setting architecture flags for ${arch}") - if(arch MATCHES "x86_64") - target_compile_options(${target} PRIVATE -mavx2 -mpclmul -mbmi -mlzcnt) - elseif(arch MATCHES "aarch64|arm64") + string(TOLOWER "${arch}" arch_lower) + if(arch_lower MATCHES "x86_64|amd64|x64") + if(MSVC) + target_compile_options(${target} PRIVATE /arch:AVX2 /permissive- + /Zc:__cplusplus /Zc:preprocessor) + else() + target_compile_options(${target} PRIVATE -mavx2 -mpclmul -mbmi -mlzcnt) + endif() + elseif(arch_lower MATCHES "aarch64|arm64") if(APPLE) target_compile_options(${target} PRIVATE -mcpu=apple-m1) else() @@ -12,7 +18,7 @@ function(set_arch_flags target arch) target_compile_options(${target} PRIVATE -march=armv8-a) endif() endif() - elseif(arch MATCHES "riscv64|riscv") + elseif(arch_lower MATCHES "riscv64|riscv") if(ENABLE_RISCV_ZBB) target_compile_options(${target} PRIVATE -march=rv64gc_zbb) else() diff --git a/docs/usage.md b/docs/usage.md index 7b970b5b..d495fabc 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -7,7 +7,9 @@ Examples of how to use Sonic-Cpp. ## Build with arch option Sonic-Cpp is a header-only library, you only need to add `-mavx2 -mpclmul -mbmi` -or `-march=haswell` to support. +or `-march=haswell` with GCC/Clang. With Visual Studio 2022, compile for x64 +with `/std:c++17 /arch:AVX2`; the repository CMake build adds these options +automatically. ## Basic Usage ### Parse and Serialize diff --git a/include/sonic/allocator.h b/include/sonic/allocator.h index 3858f20a..196501dc 100644 --- a/include/sonic/allocator.h +++ b/include/sonic/allocator.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -88,14 +89,7 @@ class SpinLock { // use pause or yield instruction will slow down lock acquisition // on contended locks. #ifndef SONIC_SPINLOCK_NO_PAUSE - -#if defined(__x86_64__) || defined(_M_AMD64) - __builtin_ia32_pause(); -#elif defined(__aarch64__) || defined(_M_ARM64) - asm volatile("yield"); -#elif defined(__riscv) - __asm__ volatile("pause"); -#endif + internal::CpuRelax(); #endif } } @@ -157,7 +151,7 @@ class AdaptiveChunkPolicy { if (min_chunk_size_ < need_alloc_size && min_chunk_size_ < SONIC_ALLOCATOR_MAX_CHUNK_CAPACITY) { size_t p = - 1ULL << (64 - __builtin_clzll( + 1ULL << (64 - internal::CountLeadingZeros64( need_alloc_size)); // size > 0 && never shift 64 min_chunk_size_ = p < SONIC_ALLOCATOR_MAX_CHUNK_CAPACITY ? p diff --git a/include/sonic/dom/serialize.h b/include/sonic/dom/serialize.h index d89327db..03ac49ce 100644 --- a/include/sonic/dom/serialize.h +++ b/include/sonic/dom/serialize.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include "sonic/dom/flags.h" @@ -51,9 +52,9 @@ sonic_force_inline SonicError SerializeImpl(const NodeType* node, uint32_t member_cnt = 0; uint32_t val_cnt, val_cnt_nxt; size_t str_len; - long inc_len; + size_t inc_len; const char* str_ptr; - ssize_t rn = 0; + std::ptrdiff_t rn = 0; internal::Stack stk; ParentCtx* parent; if constexpr ((serializeFlags & SerializeFlags::kSerializeAppendBuffer) == @@ -84,7 +85,7 @@ sonic_force_inline SonicError SerializeImpl(const NodeType* node, str_ptr = node->GetStringView().data(); rn = internal::Quote(str_ptr, str_len, wb.End()) - wb.End(); - wb.PushSizeUnsafe(rn); + wb.PushSizeUnsafe(static_cast(rn)); wb.PushUnsafe(is_key ? ':' : ','); member_cnt -= is_key; break; @@ -138,7 +139,7 @@ sonic_force_inline SonicError SerializeImpl(const NodeType* node, break; } sonic_assert(rn >= 0 && rn <= 32); - wb.PushSizeUnsafe(rn); + wb.PushSizeUnsafe(static_cast(rn)); wb.PushUnsafe(','); break; } diff --git a/include/sonic/internal/arch/avx2/base.h b/include/sonic/internal/arch/avx2/base.h index aa867754..8d2b5ef9 100644 --- a/include/sonic/internal/arch/avx2/base.h +++ b/include/sonic/internal/arch/avx2/base.h @@ -63,12 +63,12 @@ sonic_force_inline int TrailingZeroes(uint64_t input_num) { // return (int)_tzcnt_u64(input_num); // but the generated code differs and might be less efficient? //////// - return __builtin_ctzll(input_num); + return CountTrailingZeros64(input_num); } /* result might be undefined when input_num is zero */ sonic_force_inline uint64_t ClearLowestBit(uint64_t input_num) { -#if __BMI__ +#if defined(__BMI__) return _blsr_u64(input_num); #else return input_num & (input_num - 1); @@ -77,17 +77,17 @@ sonic_force_inline uint64_t ClearLowestBit(uint64_t input_num) { /* result might be undefined when input_num is zero */ sonic_force_inline int LeadingZeroes(uint64_t input_num) { - return __builtin_clzll(input_num); + return CountLeadingZeros64(input_num); } sonic_force_inline long long int CountOnes(uint64_t input_num) { - return __builtin_popcountll(input_num); + return CountOnes64(input_num); } sonic_force_inline uint64_t PrefixXor(const uint64_t bitmask) { // There should be no such thing with a processor supporting avx2 // but not clmul. -#if __PCLMUL__ +#if defined(__PCLMUL__) || defined(_MSC_VER) __m128i all_ones = _mm_set1_epi8('\xFF'); __m128i result = _mm_clmulepi64_si128(_mm_set_epi64x(0ULL, bitmask), all_ones, 0); @@ -217,13 +217,10 @@ static sonic_force_inline int cmp_lt_32(const void* _l, const void* _r, #pragma GCC diagnostic pop #endif __m256i ans = _mm256_cmpeq_epi8(vec_l, vec_r); - int mask = _mm256_movemask_epi8(ans) + 1; - // mask = mask << (32 -s); - __asm__("bzhil %1, %2, %[result]\n\t" - : [result] "=r"(mask) - : "r"((int)s), "r"(mask)); + uint32_t mask = static_cast(_mm256_movemask_epi8(ans)) + 1; + mask &= (uint32_t{1} << s) - 1; if (mask) { - int ne_idx = __builtin_ctz(mask); + int ne_idx = CountTrailingZeros32(mask); // if (lhs[ne_idx] < rhs[ne_idx]) return -1; // else return 1; return lhs[ne_idx] - rhs[ne_idx]; @@ -254,14 +251,14 @@ static inline bool is_eq_lt_32_cross_page(const void* _a, const void* _b, } // cross page if (s >= 8) { - return __builtin_memcmp(a, b, 8) == 0 && - __builtin_memcmp(a + s - 8, b + s - 8, 8) == 0; + return std::memcmp(a, b, 8) == 0 && + std::memcmp(a + s - 8, b + s - 8, 8) == 0; } else if (s >= 4) { - return __builtin_memcmp(a, b, 4) == 0 && - __builtin_memcmp(a + s - 4, b + s - 4, 4) == 0; + return std::memcmp(a, b, 4) == 0 && + std::memcmp(a + s - 4, b + s - 4, 4) == 0; } else if (s >= 2) { - return __builtin_memcmp(a, b, 2) == 0 && - __builtin_memcmp(a + s - 2, b + s - 2, 2) == 0; + return std::memcmp(a, b, 2) == 0 && + std::memcmp(a + s - 2, b + s - 2, 2) == 0; } else { return *a == *b; } @@ -276,11 +273,8 @@ static sonic_force_inline bool is_eq_lt_32(const void* _a, const void* _b, __m256i vec_a = _mm256_loadu_si256((__m256i const*)a); __m256i vec_b = _mm256_loadu_si256((__m256i const*)b); __m256i ans = _mm256_cmpeq_epi8(vec_a, vec_b); - int mask = _mm256_movemask_epi8(ans) + 1; - // mask = mask << (32 -s); - __asm__("bzhil %1, %2, %[result]\n\t" - : [result] "=r"(mask) - : "r"((int)s), "r"(mask)); + uint32_t mask = static_cast(_mm256_movemask_epi8(ans)) + 1; + mask &= (uint32_t{1} << s) - 1; return mask == 0; } return is_eq_lt_32_cross_page(a, b, s); @@ -334,7 +328,7 @@ sonic_force_inline int InlinedMemcmp(const void* _l, const void* _r, size_t s) { __m256i ans_1 = _mm256_cmpeq_epi8(vec_l, vec_r); uint32_t mask = static_cast(_mm256_movemask_epi8(ans_1)) + 1; if (mask) { - int ne_idx = __builtin_ctz(mask); + int ne_idx = CountTrailingZeros32(mask); // if (lhs[ne_idx] < rhs[ne_idx]) return -1; // else return 1; return lhs[ne_idx] - rhs[ne_idx]; @@ -346,7 +340,7 @@ sonic_force_inline int InlinedMemcmp(const void* _l, const void* _r, size_t s) { __m256i ans = _mm256_cmpeq_epi8(vec_l, vec_r); mask = static_cast(_mm256_movemask_epi8(ans)) + 1; if (mask) { - int ne_idx = __builtin_ctz(mask); + int ne_idx = CountTrailingZeros32(mask); // if (lhs[i + ne_idx] < rhs[i + ne_idx]) return -1; // else return 1; return lhs[i + ne_idx] - rhs[i + ne_idx]; @@ -363,7 +357,7 @@ sonic_force_inline int InlinedMemcmp(const void* _l, const void* _r, size_t s) { // ans = _mm256_and_si256(ans, ans_1); unsigned int mask = static_cast(_mm256_movemask_epi8(ans)) + 1; if (mask) { - int ne_idx = __builtin_ctz(mask); + int ne_idx = CountTrailingZeros32(mask); return lhs[offset + ne_idx] - rhs[offset + ne_idx]; } } diff --git a/include/sonic/internal/arch/common/quote_tables.h b/include/sonic/internal/arch/common/quote_tables.h index 7d396fc9..dbd600c0 100644 --- a/include/sonic/internal/arch/common/quote_tables.h +++ b/include/sonic/internal/arch/common/quote_tables.h @@ -51,7 +51,7 @@ static const uint8_t kEscapedMap[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; -// GCC didn't support non-trivial designated initializers C99 extension +// Keep entries as positional aggregate initializers for C++17 portability. struct QuotedChar { long n; const char *s; @@ -59,42 +59,42 @@ struct QuotedChar { static const struct QuotedChar kQuoteTabLowerCase[256] = { // 0x00 ~ 0x1f - {.n = 6, .s = "\\u0000\0\0"}, - {.n = 6, .s = "\\u0001\0\0"}, - {.n = 6, .s = "\\u0002\0\0"}, - {.n = 6, .s = "\\u0003\0\0"}, - {.n = 6, .s = "\\u0004\0\0"}, - {.n = 6, .s = "\\u0005\0\0"}, - {.n = 6, .s = "\\u0006\0\0"}, - {.n = 6, .s = "\\u0007\0\0"}, - {.n = 2, .s = "\\b\0\0\0\0\0\0"}, - {.n = 2, .s = "\\t\0\0\0\0\0\0"}, - {.n = 2, .s = "\\n\0\0\0\0\0\0"}, - {.n = 6, .s = "\\u000b\0\0"}, - {.n = 2, .s = "\\f\0\0\0\0\0\0"}, - {.n = 2, .s = "\\r\0\0\0\0\0\0"}, - {.n = 6, .s = "\\u000e\0\0"}, - {.n = 6, .s = "\\u000f\0\0"}, - {.n = 6, .s = "\\u0010\0\0"}, - {.n = 6, .s = "\\u0011\0\0"}, - {.n = 6, .s = "\\u0012\0\0"}, - {.n = 6, .s = "\\u0013\0\0"}, - {.n = 6, .s = "\\u0014\0\0"}, - {.n = 6, .s = "\\u0015\0\0"}, - {.n = 6, .s = "\\u0016\0\0"}, - {.n = 6, .s = "\\u0017\0\0"}, - {.n = 6, .s = "\\u0018\0\0"}, - {.n = 6, .s = "\\u0019\0\0"}, - {.n = 6, .s = "\\u001a\0\0"}, - {.n = 6, .s = "\\u001b\0\0"}, - {.n = 6, .s = "\\u001c\0\0"}, - {.n = 6, .s = "\\u001d\0\0"}, - {.n = 6, .s = "\\u001e\0\0"}, - {.n = 6, .s = "\\u001f\0\0"}, + {6, "\\u0000\0\0"}, + {6, "\\u0001\0\0"}, + {6, "\\u0002\0\0"}, + {6, "\\u0003\0\0"}, + {6, "\\u0004\0\0"}, + {6, "\\u0005\0\0"}, + {6, "\\u0006\0\0"}, + {6, "\\u0007\0\0"}, + {2, "\\b\0\0\0\0\0\0"}, + {2, "\\t\0\0\0\0\0\0"}, + {2, "\\n\0\0\0\0\0\0"}, + {6, "\\u000b\0\0"}, + {2, "\\f\0\0\0\0\0\0"}, + {2, "\\r\0\0\0\0\0\0"}, + {6, "\\u000e\0\0"}, + {6, "\\u000f\0\0"}, + {6, "\\u0010\0\0"}, + {6, "\\u0011\0\0"}, + {6, "\\u0012\0\0"}, + {6, "\\u0013\0\0"}, + {6, "\\u0014\0\0"}, + {6, "\\u0015\0\0"}, + {6, "\\u0016\0\0"}, + {6, "\\u0017\0\0"}, + {6, "\\u0018\0\0"}, + {6, "\\u0019\0\0"}, + {6, "\\u001a\0\0"}, + {6, "\\u001b\0\0"}, + {6, "\\u001c\0\0"}, + {6, "\\u001d\0\0"}, + {6, "\\u001e\0\0"}, + {6, "\\u001f\0\0"}, // 0x20 ~ 0x2f {0, 0}, {0, 0}, - {.n = 2, .s = "\\\"\0\0\0\0\0\0"}, + {2, "\\\"\0\0\0\0\0\0"}, {0, 0}, {0, 0}, {0, 0}, @@ -154,7 +154,7 @@ static const struct QuotedChar kQuoteTabLowerCase[256] = { {0, 0}, {0, 0}, {0, 0}, - {.n = 2, .s = "\\\\\0\0\0\0\0\0"}, + {2, "\\\\\0\0\0\0\0\0"}, {0, 0}, {0, 0}, {0, 0}, @@ -163,42 +163,42 @@ static const struct QuotedChar kQuoteTabLowerCase[256] = { static const struct QuotedChar kQuoteTabUpperCase[256] = { // 0x00 ~ 0x1f - {.n = 6, .s = "\\u0000\0\0"}, - {.n = 6, .s = "\\u0001\0\0"}, - {.n = 6, .s = "\\u0002\0\0"}, - {.n = 6, .s = "\\u0003\0\0"}, - {.n = 6, .s = "\\u0004\0\0"}, - {.n = 6, .s = "\\u0005\0\0"}, - {.n = 6, .s = "\\u0006\0\0"}, - {.n = 6, .s = "\\u0007\0\0"}, - {.n = 2, .s = "\\b\0\0\0\0\0\0"}, - {.n = 2, .s = "\\t\0\0\0\0\0\0"}, - {.n = 2, .s = "\\n\0\0\0\0\0\0"}, - {.n = 6, .s = "\\u000B\0\0"}, - {.n = 2, .s = "\\f\0\0\0\0\0\0"}, - {.n = 2, .s = "\\r\0\0\0\0\0\0"}, - {.n = 6, .s = "\\u000E\0\0"}, - {.n = 6, .s = "\\u000F\0\0"}, - {.n = 6, .s = "\\u0010\0\0"}, - {.n = 6, .s = "\\u0011\0\0"}, - {.n = 6, .s = "\\u0012\0\0"}, - {.n = 6, .s = "\\u0013\0\0"}, - {.n = 6, .s = "\\u0014\0\0"}, - {.n = 6, .s = "\\u0015\0\0"}, - {.n = 6, .s = "\\u0016\0\0"}, - {.n = 6, .s = "\\u0017\0\0"}, - {.n = 6, .s = "\\u0018\0\0"}, - {.n = 6, .s = "\\u0019\0\0"}, - {.n = 6, .s = "\\u001A\0\0"}, - {.n = 6, .s = "\\u001B\0\0"}, - {.n = 6, .s = "\\u001C\0\0"}, - {.n = 6, .s = "\\u001D\0\0"}, - {.n = 6, .s = "\\u001E\0\0"}, - {.n = 6, .s = "\\u001F\0\0"}, + {6, "\\u0000\0\0"}, + {6, "\\u0001\0\0"}, + {6, "\\u0002\0\0"}, + {6, "\\u0003\0\0"}, + {6, "\\u0004\0\0"}, + {6, "\\u0005\0\0"}, + {6, "\\u0006\0\0"}, + {6, "\\u0007\0\0"}, + {2, "\\b\0\0\0\0\0\0"}, + {2, "\\t\0\0\0\0\0\0"}, + {2, "\\n\0\0\0\0\0\0"}, + {6, "\\u000B\0\0"}, + {2, "\\f\0\0\0\0\0\0"}, + {2, "\\r\0\0\0\0\0\0"}, + {6, "\\u000E\0\0"}, + {6, "\\u000F\0\0"}, + {6, "\\u0010\0\0"}, + {6, "\\u0011\0\0"}, + {6, "\\u0012\0\0"}, + {6, "\\u0013\0\0"}, + {6, "\\u0014\0\0"}, + {6, "\\u0015\0\0"}, + {6, "\\u0016\0\0"}, + {6, "\\u0017\0\0"}, + {6, "\\u0018\0\0"}, + {6, "\\u0019\0\0"}, + {6, "\\u001A\0\0"}, + {6, "\\u001B\0\0"}, + {6, "\\u001C\0\0"}, + {6, "\\u001D\0\0"}, + {6, "\\u001E\0\0"}, + {6, "\\u001F\0\0"}, // 0x20 ~ 0x2f {0, 0}, {0, 0}, - {.n = 2, .s = "\\\"\0\0\0\0\0\0"}, + {2, "\\\"\0\0\0\0\0\0"}, {0, 0}, {0, 0}, {0, 0}, @@ -258,7 +258,7 @@ static const struct QuotedChar kQuoteTabUpperCase[256] = { {0, 0}, {0, 0}, {0, 0}, - {.n = 2, .s = "\\\\\0\0\0\0\0\0"}, + {2, "\\\\\0\0\0\0\0\0"}, {0, 0}, {0, 0}, {0, 0}, diff --git a/include/sonic/internal/arch/common/x86_common/itoa.h b/include/sonic/internal/arch/common/x86_common/itoa.h index ca6e3fe3..e60fff96 100644 --- a/include/sonic/internal/arch/common/x86_common/itoa.h +++ b/include/sonic/internal/arch/common/x86_common/itoa.h @@ -28,8 +28,8 @@ #elif defined(__GNUG__) #pragma GCC push_options #pragma GCC target("sse,sse2,sse3,sse4.1") -#else -#error "Only g++ and clang is supported!" +#elif !defined(_MSC_VER) +#error "Unsupported compiler" #endif namespace sonic_json { @@ -43,34 +43,34 @@ namespace x86_common { #define as_m128v(v) (*(const __m128i *)(v)) #define as_uint64v(p) (*(uint64_t *)(p)) -static const char kVec16xAsc0[16] sonic_align(16) = { +sonic_align(16) static const char kVec16xAsc0[16] = { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', }; -static const uint16_t kVec8x10[8] sonic_align(16) = { +sonic_align(16) static const uint16_t kVec8x10[8] = { 10, 10, 10, 10, 10, 10, 10, 10, }; -static const uint32_t kVec4x10k[4] sonic_align(16) = { +sonic_align(16) static const uint32_t kVec4x10k[4] = { 10000, 10000, 10000, 10000, }; -static const uint32_t kVec4xDiv10k[4] sonic_align(16) = { +sonic_align(16) static const uint32_t kVec4xDiv10k[4] = { 0xd1b71759, 0xd1b71759, 0xd1b71759, 0xd1b71759, }; -static const uint16_t kVecDivPowers[8] sonic_align(16) = { +sonic_align(16) static const uint16_t kVecDivPowers[8] = { 0x20c5, 0x147b, 0x3334, 0x8000, 0x20c5, 0x147b, 0x3334, 0x8000, }; -static const uint16_t kVecShiftPowers[8] sonic_align(16) = { +sonic_align(16) static const uint16_t kVecShiftPowers[8] = { 0x0080, 0x0800, 0x2000, 0x8000, 0x0080, 0x0800, 0x2000, 0x8000, }; diff --git a/include/sonic/internal/arch/common/x86_common/quote.inc.h b/include/sonic/internal/arch/common/x86_common/quote.inc.h index 1a84f2d6..861daf6b 100644 --- a/include/sonic/internal/arch/common/x86_common/quote.inc.h +++ b/include/sonic/internal/arch/common/x86_common/quote.inc.h @@ -185,7 +185,7 @@ sonic_static_inline char *Quote(const char *src, size_t nb, char *dst) { /* check for matches */ // TODO: optimize: exploit the simd bitmask in the escape block. if ((mm = CopyAndGetEscapMask(src, dst)) != 0) { - cn = __builtin_ctz(mm); + cn = CountTrailingZeros32(mm); MOVE_N_CHARS(src, cn); DoEscape(src, dst, nb); } else { @@ -212,7 +212,7 @@ sonic_static_inline char *Quote(const char *src, size_t nb, char *dst) { mm = CopyAndGetEscapMask(src_r, dst) & (VEC_FULL_MASK >> (VEC_LEN - nb)); if (mm) { - cn = __builtin_ctz(mm); + cn = CountTrailingZeros32(mm); MOVE_N_CHARS(src_r, cn); DoEscape(src_r, dst, nb); } else { diff --git a/include/sonic/internal/arch/simd_dispatch.h b/include/sonic/internal/arch/simd_dispatch.h index a8ee07f7..7cdf5126 100644 --- a/include/sonic/internal/arch/simd_dispatch.h +++ b/include/sonic/internal/arch/simd_dispatch.h @@ -18,6 +18,10 @@ #include "../../macro.h" #include "sonic_cpu_feature.h" +#if defined(_MSC_VER) && defined(SONIC_DYNAMIC_DISPATCH) +#error "MSVC supports SONIC_STATIC_DISPATCH only; compile with /arch:AVX2" +#endif + #ifndef SONIC_DYNAMIC_DISPATCH #define SONIC_STATIC_DISPATCH #endif diff --git a/include/sonic/internal/arch/sonic_cpu_feature.h b/include/sonic/internal/arch/sonic_cpu_feature.h index 17222fb9..75c9fe09 100644 --- a/include/sonic/internal/arch/sonic_cpu_feature.h +++ b/include/sonic/internal/arch/sonic_cpu_feature.h @@ -16,7 +16,7 @@ #pragma once -#if defined(__SSE2__) +#if defined(__SSE2__) || (defined(_MSC_VER) && defined(__AVX2__)) #define SONIC_HAVE_SSE #define SONIC_HAVE_SSE2 #if defined(__SSE3__) diff --git a/include/sonic/internal/arch/sse/base.h b/include/sonic/internal/arch/sse/base.h index 725af3c6..dad0cac3 100644 --- a/include/sonic/internal/arch/sse/base.h +++ b/include/sonic/internal/arch/sse/base.h @@ -36,12 +36,12 @@ sonic_force_inline int TrailingZeroes(uint64_t input_num) { // return (int)_tzcnt_u64(input_num); // but the generated code differs and might be less efficient? //////// - return __builtin_ctzll(input_num); + return CountTrailingZeros64(input_num); } /* result might be undefined when input_num is zero */ sonic_force_inline uint64_t ClearLowestBit(uint64_t input_num) { -#if __BMI__ +#if defined(__BMI__) return _blsr_u64(input_num); #else return input_num & (input_num - 1); @@ -50,15 +50,15 @@ sonic_force_inline uint64_t ClearLowestBit(uint64_t input_num) { /* result might be undefined when input_num is zero */ sonic_force_inline int LeadingZeroes(uint64_t input_num) { - return __builtin_clzll(input_num); + return CountLeadingZeros64(input_num); } sonic_force_inline long long int CountOnes(uint64_t input_num) { - return __builtin_popcountll(input_num); + return CountOnes64(input_num); } sonic_force_inline uint64_t PrefixXor(const uint64_t bitmask) { -#if __PCLMUL__ +#if defined(__PCLMUL__) || defined(_MSC_VER) __m128i all_ones = _mm_set1_epi8('\xFF'); __m128i result = _mm_clmulepi64_si128(_mm_set_epi64x(0ULL, bitmask), all_ones, 0); diff --git a/include/sonic/internal/arch/sse/str2int.h b/include/sonic/internal/arch/sse/str2int.h index b1e77605..2619dcc5 100644 --- a/include/sonic/internal/arch/sse/str2int.h +++ b/include/sonic/internal/arch/sse/str2int.h @@ -71,7 +71,7 @@ sonic_force_inline uint64_t simd_str2int(const char* c, int& man_nd) { int is_num_end_int = _mm_movemask_epi8(is_num_end); if (is_num_end_int) { - num_end_idx = __builtin_ctz(is_num_end_int); + num_end_idx = CountTrailingZeros32(static_cast(is_num_end_int)); } man_nd = man_nd < num_end_idx ? man_nd : num_end_idx; diff --git a/include/sonic/internal/ftoa.h b/include/sonic/internal/ftoa.h index 8a30abf8..5949c68e 100644 --- a/include/sonic/internal/ftoa.h +++ b/include/sonic/internal/ftoa.h @@ -693,7 +693,7 @@ static sonic_force_inline const uint64x2 Pow10CeilSig(int32_t k) { #undef KMAX } -#if __SIZEOF_INT128__ +#if defined(__SIZEOF_INT128__) __extension__ using uint128_t = __uint128_t; diff --git a/include/sonic/internal/itoa.h b/include/sonic/internal/itoa.h index aef17ea3..a7ce934f 100644 --- a/include/sonic/internal/itoa.h +++ b/include/sonic/internal/itoa.h @@ -27,7 +27,7 @@ namespace sonic_json { namespace internal { -static const char kDigits[202] sonic_align(2) = +sonic_align(2) static const char kDigits[202] = "00010203040506070809" "10111213141516171819" "20212223242526272829" diff --git a/include/sonic/macro.h b/include/sonic/macro.h index e1fb37ca..621232a0 100644 --- a/include/sonic/macro.h +++ b/include/sonic/macro.h @@ -16,16 +16,108 @@ #pragma once +#include + +#if defined(_MSC_VER) +#include +#endif + #define SONICJSON_PADDING 64 +#if defined(_MSC_VER) +#define sonic_likely(v) (v) +#define sonic_align(s) alignas(s) +#define sonic_unlikely(v) (v) +#define sonic_force_inline __forceinline +#define sonic_never_inline __declspec(noinline) inline +#else #define sonic_likely(v) (__builtin_expect((v), 1)) -#define sonic_align(s) __attribute__((aligned(s))) +#define sonic_align(s) alignas(s) #define sonic_unlikely(v) (__builtin_expect((v), 0)) #define sonic_force_inline inline __attribute__((always_inline)) #define sonic_never_inline inline __attribute__((noinline)) +#endif #define sonic_static_noinline static sonic_never_inline #define sonic_static_inline static sonic_force_inline +namespace sonic_json { +namespace internal { + +static sonic_force_inline int CountTrailingZeros32(uint32_t value) { + if (value == 0) return 32; +#if defined(_MSC_VER) + unsigned long index; + _BitScanForward(&index, static_cast(value)); + return static_cast(index); +#else + return __builtin_ctz(value); +#endif +} + +static sonic_force_inline int CountTrailingZeros64(uint64_t value) { + if (value == 0) return 64; +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index; + _BitScanForward64(&index, value); + return static_cast(index); +#elif defined(_MSC_VER) + const uint32_t low = static_cast(value); + return low != 0 + ? CountTrailingZeros32(low) + : 32 + CountTrailingZeros32(static_cast(value >> 32)); +#else + return __builtin_ctzll(value); +#endif +} + +static sonic_force_inline int CountLeadingZeros64(uint64_t value) { + if (value == 0) return 64; +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index; + _BitScanReverse64(&index, value); + return 63 - static_cast(index); +#elif defined(_MSC_VER) + unsigned long index; + const uint32_t high = static_cast(value >> 32); + if (high != 0) { + _BitScanReverse(&index, static_cast(high)); + return 31 - static_cast(index); + } + _BitScanReverse(&index, static_cast(value)); + return 63 - static_cast(index); +#else + return __builtin_clzll(value); +#endif +} + +static sonic_force_inline long long CountOnes64(uint64_t value) { +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + return static_cast(__popcnt64(value)); +#elif defined(_MSC_VER) + return static_cast(__popcnt(static_cast(value)) + + __popcnt(static_cast(value >> 32))); +#else + return static_cast(__builtin_popcountll(value)); +#endif +} + +static sonic_force_inline void CpuRelax() { +#if defined(_MSC_VER) && defined(_M_ARM64) + __yield(); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) + _mm_pause(); +#elif defined(__x86_64__) || defined(__i386__) + __builtin_ia32_pause(); +#elif defined(__aarch64__) + asm volatile("yield"); +#elif defined(__riscv) + __asm__ volatile("pause"); +#endif +} + +} // namespace internal +} // namespace sonic_json + #ifdef SONIC_DEBUG #include #include @@ -46,7 +138,8 @@ #define sonic_assert(x) assert((x)) #endif -#if __cplusplus >= 201703L +#if (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || \ + (!defined(_MSVC_LANG) && __cplusplus >= 201703L) #define SONIC_IF_CONSTEXPR constexpr #else #define SONIC_IF_CONSTEXPR @@ -71,6 +164,13 @@ #define SONIC_PUSH_TARGET(_target) \ _Pragma("GCC push_options") _Pragma(SONIC_STRINGIFY(GCC target(_target))) #define SONIC_POP_TARGET _Pragma("GCC pop_options") +#elif defined(_MSC_VER) +// MSVC selects the target ISA for the translation unit (for example, +// /arch:AVX2) and does not support per-function target attributes. +#define SONIC_PUSH_TARGET(_target) +#define SONIC_POP_TARGET +#else +#error "Unsupported compiler" #endif #define SONIC_PUSH_WESTMERE SONIC_PUSH_TARGET(SONIC_WESTMERE) diff --git a/include/sonic/string_view.h b/include/sonic/string_view.h index 67dfe2f3..fdf1c4b4 100644 --- a/include/sonic/string_view.h +++ b/include/sonic/string_view.h @@ -15,7 +15,8 @@ */ #pragma once -#if __cplusplus >= 201703L +#if (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || \ + (!defined(_MSVC_LANG) && __cplusplus >= 201703L) #include namespace sonic_json { using StringView = std::string_view; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a4fd2725..c4c8c721 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,8 +21,14 @@ FetchContent_Declare( set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) -# for arm CI: ASAN is very annoying when dealing with qemu -option(ENABLE_ASAN "Enable AddressSanitizer" ON) +# for arm CI: ASAN is very annoying when dealing with qemu. Keep it opt-in on +# MSVC as well because Visual Studio installations may omit the ASan workload. +if(MSVC) + set(ENABLE_ASAN_DEFAULT OFF) +else() + set(ENABLE_ASAN_DEFAULT ON) +endif() +option(ENABLE_ASAN "Enable AddressSanitizer" ${ENABLE_ASAN_DEFAULT}) file(GLOB SONIC_TEST_FILES "${PROJECT_SOURCE_DIR}/tests/*.h" @@ -34,14 +40,20 @@ target_link_libraries(unittest PRIVATE gtest_main) target_include_directories(unittest PRIVATE ${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}) if(ENABLE_ASAN) - if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(MSVC) + target_compile_options(unittest PRIVATE /fsanitize=address /Zi /W4) + target_link_options(unittest PRIVATE /INCREMENTAL:NO) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(unittest PRIVATE -O0 -g -fsanitize=address -Werror -Wall -Wextra-semi -Wextra-semi-stmt -Wcomma) + target_link_options(unittest PRIVATE -fsanitize=address) elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(unittest PRIVATE -O0 -g -fsanitize=address -Werror -Wall) + target_link_options(unittest PRIVATE -fsanitize=address) endif() - target_link_options(unittest PRIVATE -fsanitize=address) else() - if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(MSVC) + target_compile_options(unittest PRIVATE /W4) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(unittest PRIVATE -O0 -g -Werror -Wall -Wextra-semi -Wextra-semi-stmt -Wcomma) elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(unittest PRIVATE -O0 -g -Werror -Wall -Wno-use-after-free) @@ -51,3 +63,5 @@ endif() include("${PROJECT_SOURCE_DIR}/cmake/set_arch_flags.cmake") set_arch_flags(unittest ${CMAKE_SYSTEM_PROCESSOR}) add_test(NAME sonic-unittest COMMAND unittest) +set_tests_properties(sonic-unittest PROPERTIES + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) diff --git a/tests/allocator_test.cpp b/tests/allocator_test.cpp index 5a44e5f4..03f9cb99 100644 --- a/tests/allocator_test.cpp +++ b/tests/allocator_test.cpp @@ -24,9 +24,11 @@ // Let huge-allocation OOM tests return null under ASAN instead of aborting. // Dead code in non-ASAN builds; ASAN_OPTIONS still overrides it. +#if defined(__GNUC__) || defined(__clang__) extern "C" __attribute__((used)) const char *__asan_default_options() { return "allocator_may_return_null=1"; } +#endif namespace { diff --git a/tests/document_test.cpp b/tests/document_test.cpp index 5db2398b..f1c2376f 100644 --- a/tests/document_test.cpp +++ b/tests/document_test.cpp @@ -14,9 +14,8 @@ * limitations under the License. */ -#include - #include +#include #include #include #include @@ -344,35 +343,18 @@ static std::string get_json(const std::string& file) { } static std::vector get_all_jsons(const std::string& dirname) { - DIR* dir = opendir(dirname.c_str()); std::vector jsons; - EXPECT_TRUE(dir != nullptr) << "Error open directory: " + dirname; - if (nullptr == dir) return jsons; - struct dirent* ent = nullptr; - while (nullptr != (ent = readdir(dir))) { - struct stat st; - std::string filename = dirname + ent->d_name; - int ret = lstat(filename.c_str(), &st); - if (ret == -1) { - goto return_label; - } - if (S_ISDIR(st.st_mode)) { - if (ent->d_name[0] == '.') { - continue; - } - auto sub_jsons = get_all_jsons(filename + '/'); - jsons.insert(jsons.end(), sub_jsons.begin(), sub_jsons.end()); - } else { - auto const pos = filename.find_last_of('.'); - const auto extension = filename.substr(pos + 1); - if (extension == "json") { - jsons.push_back(get_json(filename)); - } + const std::filesystem::path root(dirname); + EXPECT_TRUE(std::filesystem::is_directory(root)) + << "Error open directory: " + dirname; + if (!std::filesystem::is_directory(root)) return jsons; + + for (const auto& entry : std::filesystem::recursive_directory_iterator( + root, std::filesystem::directory_options::skip_permission_denied)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + jsons.push_back(get_json(entry.path().string())); } } - -return_label: - closedir(dir); return jsons; } diff --git a/tests/exp_update_test.cpp b/tests/exp_update_test.cpp index 76da60b7..b23b5e06 100644 --- a/tests/exp_update_test.cpp +++ b/tests/exp_update_test.cpp @@ -30,85 +30,85 @@ TEST(UpdateLazy, Basic) { }; std::vector tests = { { - .target = "null", - .source = "123", - .updated = "123", + "null", + "123", + "123", }, { - .target = "[]", - .source = R"({"a":1})", - .updated = R"({"a":1})", + "[]", + R"({"a":1})", + R"({"a":1})", }, { - .target = "[1,2,3]", - .source = R"([4,5,6])", - .updated = R"([4,5,6])", + "[1,2,3]", + R"([4,5,6])", + R"([4,5,6])", }, { - .target = R"({"a":2})", - .source = R"({"a":1})", - .updated = R"({"a":1})", + R"({"a":2})", + R"({"a":1})", + R"({"a":1})", }, { - .target = R"({"a":{"b":0}})", - .source = R"({"a":{"b":1}})", - .updated = R"({"a":{"b":1}})", + R"({"a":{"b":0}})", + R"({"a":{"b":1}})", + R"({"a":{"b":1}})", }, { - .target = R"({"a":{"b":0}})", - .source = R"({"a":{"c":1}})", - .updated = R"({"a":{"b":0,"c":1}})", + R"({"a":{"b":0}})", + R"({"a":{"c":1}})", + R"({"a":{"b":0,"c":1}})", }, { - .target = R"({"a":{"b":0,"c":0}})", - .source = R"({"a":{"c":1}})", - .updated = R"({"a":{"b":0,"c":1}})", + R"({"a":{"b":0,"c":0}})", + R"({"a":{"c":1}})", + R"({"a":{"b":0,"c":1}})", }, { - .target = R"({"a":{"b":0,"c":0,"d":0}})", - .source = R"({"a":{"c":1}})", - .updated = R"({"a":{"b":0,"c":1,"d":0}})", + R"({"a":{"b":0,"c":0,"d":0}})", + R"({"a":{"c":1}})", + R"({"a":{"b":0,"c":1,"d":0}})", }, { - .target = R"({"b":0,"c":0,"d":0})", - .source = R"({"c":1})", - .updated = R"({"b":0,"c":1,"d":0})", + R"({"b":0,"c":0,"d":0})", + R"({"c":1})", + R"({"b":0,"c":1,"d":0})", }, { - .target = R"({"a":{"b":0, "c":0,"d":0}})", - .source = R"({"a":{"d":1}})", - .updated = R"({"a":{"b":0,"c":0,"d":1}})", + R"({"a":{"b":0, "c":0,"d":0}})", + R"({"a":{"d":1}})", + R"({"a":{"b":0,"c":0,"d":1}})", }, { - .target = R"({"a":{"b":0,"c":0, "d":0}})", - .source = R"({"a":{"b":1,"e":1}})", - .updated = R"({"a":{"b":1,"c":0,"d":0,"e":1}})", + R"({"a":{"b":0,"c":0, "d":0}})", + R"({"a":{"b":1,"e":1}})", + R"({"a":{"b":1,"c":0,"d":0,"e":1}})", }, { - .target = R"({"d":1})", - .source = R"({"b":0, "c":0,"d":0})", - .updated = R"({"d":0,"b":0,"c":0})", + R"({"d":1})", + R"({"b":0, "c":0,"d":0})", + R"({"d":0,"b":0,"c":0})", }, { - .target = R"({"a":{"d":1}})", - .source = R"({"a":{"b":0, "c":0,"d":0}})", - .updated = R"({"a":{"d":0,"b":0,"c":0}})", + R"({"a":{"d":1}})", + R"({"a":{"b":0, "c":0,"d":0}})", + R"({"a":{"d":0,"b":0,"c":0}})", }, { - .target = R"({"a":{"b":1,"e":1}})", - .source = R"({"a":{"b":0,"c":0, "d":0}})", - .updated = R"({"a":{"b":0,"e":1,"c":0,"d":0}})", + R"({"a":{"b":1,"e":1}})", + R"({"a":{"b":0,"c":0, "d":0}})", + R"({"a":{"b":0,"e":1,"c":0,"d":0}})", }, { - .target = R"({"a":{"b":0,"x":0}})", - .source = R"({"a":{"c":1,"b":1}})", - .updated = R"({"a":{"b":1,"x":0,"c":1}})", + R"({"a":{"b":0,"x":0}})", + R"({"a":{"c":1,"b":1}})", + R"({"a":{"b":1,"x":0,"c":1}})", }, { - .target = R"({"a":{"b":0,"x":0}})", - .source = R"({"a":{"c":{"d":"xxxxxx"},"b":1}})", - .updated = R"({"a":{"b":1,"x":0,"c":{"d":"xxxxxx"}}})", + R"({"a":{"b":0,"x":0}})", + R"({"a":{"c":{"d":"xxxxxx"},"b":1}})", + R"({"a":{"b":1,"x":0,"c":{"d":"xxxxxx"}}})", }, }; diff --git a/tests/memcmp_test.cpp b/tests/memcmp_test.cpp index 8d0d9fa5..9172177c 100644 --- a/tests/memcmp_test.cpp +++ b/tests/memcmp_test.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#include +#include #include #include #include @@ -89,11 +91,12 @@ TEST(InlinedMemcmp, Basic) { TEST(InlinedMemcmp, CrossPage) { for (int i = 1; i <= 1024; ++i) { std::string str = random_string(i); - auto a_ptr = std::unique_ptr( - static_cast(aligned_alloc(4096, 4096 * 2)), - [](char* ptr) { free(ptr); }); + auto a_storage = std::make_unique(4096 * 3); auto b_ptr = std::unique_ptr(new char[i]); - char* a = a_ptr.get() + 4095; + const uintptr_t aligned = + (reinterpret_cast(a_storage.get()) + 4095) & + ~uintptr_t{4095}; + char* a = reinterpret_cast(aligned) + 4095; char* b = b_ptr.get(); for (int j = 0; j < i; ++j) { std::memcpy(a, str.data(), i); @@ -143,11 +146,12 @@ TEST(InlinedMemcmpEq, CrossPage) { { std::string str = random_string(1024); for (size_t i = 1; i < 1024; ++i) { - auto a_ptr = std::unique_ptr( - static_cast(aligned_alloc(4096, 4096 * 2)), - [](char* ptr) { free(ptr); }); + auto a_storage = std::make_unique(4096 * 3); auto b = std::unique_ptr(new char[i]); - char* a = a_ptr.get() + 4095; + const uintptr_t aligned = + (reinterpret_cast(a_storage.get()) + 4095) & + ~uintptr_t{4095}; + char* a = reinterpret_cast(aligned) + 4095; std::memcpy(a, str.data(), i); std::memcpy(b.get(), str.data(), i); success_helper(a, b.get(), i); diff --git a/tests/quote_test.cpp b/tests/quote_test.cpp index c8c818eb..1f3c79bd 100644 --- a/tests/quote_test.cpp +++ b/tests/quote_test.cpp @@ -54,8 +54,8 @@ TEST(Quote, Normal) { {"\"", "\"\\\"\""}, {"\\", "\"\\\\\""}, { - "\u666fhello\b\f\n\r\t\\\"world", - R"("景hello\b\f\n\r\t\\\"world")", + u8"\u666fhello\b\f\n\r\t\\\"world", + u8"\"\u666fhello\\b\\f\\n\\r\\t\\\\\\\"world\"", }, { "