diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0972e2f..f55465e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,3 +91,27 @@ jobs: - name: Test run: ctest --test-dir build --output-on-failure -C Release + + build-and-test-scalar: + name: Linux x64 GCC (scalar) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Configure + env: + CC: gcc + CXX: g++ + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBITCAL_BUILD_TESTS=ON \ + -DBITCAL_BUILD_EXAMPLES=ON \ + -DBITCAL_NATIVE_ARCH=OFF \ + -DCMAKE_CXX_FLAGS="-mno-avx2" + + - name: Build + run: cmake --build build --config Release -j"$(nproc)" + + - name: Test + run: ctest --test-dir build --output-on-failure -C Release diff --git a/AGENTS.md b/AGENTS.md index cfd7367..5bb1c70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,10 +59,10 @@ BitCal 是一个 C++23、header-only、实验性质的 SIMD 位运算练习库 ### 4.3 工程化 -- BitCal 是 header-only 实验库;CI 只保留最小可信路径(format-check + GCC/Clang 两个 build/test job) +- BitCal 是 header-only 实验库;CI 只保留最小可信路径(format-check + GCC/Clang/scalar 三个 build/test job) - 不引入分发库基础设施(install/export/LTO/hardening/cmake config 包) - 版本号单一事实源:`include/bitcal/config.hpp`(`CMakeLists.txt` 的 `project()` 版本独立硬编码,仅为 CMake 元数据) -- scalar 路径与 sanitizer 验证在本地手动执行,不进 CI 矩阵 +- sanitizer 验证在本地手动执行,不进 CI 矩阵(scalar 路径已由 CI scalar job 覆盖) - `.clang-format` 的 `Standard` 设为 `Latest`,确保与项目语言基线(C++23)一致 ### 4.4 注释与风格 diff --git a/CHANGELOG.md b/CHANGELOG.md index bab60c4..769a108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ - `benchmarks/CMakeLists.txt` 删除冗余 `cmake_minimum_required`(子目录无需重复声明) - 删除 benchmark 文件的 Doxygen `@file`/`@brief` 头(项目无 Doxygen 工具链) - 删除 `examples/basic_usage.cpp` 与 `benchmarks/benchmark_compare.cpp` 中静态 span 改造后遗留的未使用 `#include `(`from_words` 改为接收 `std::array` 隐式转换,调用方不再显式使用 span) +- CI 新增 scalar build/test job(`-mno-avx2` + `BITCAL_NATIVE_ARCH=OFF`),覆盖 `BITCAL_HAS_AVX2 == 0` 分支;此前 CI 两个 job 均走 AVX2 路径,scalar 分支从未被编译和测试 +- `README.md` 与 `AGENTS.md` §4.3 scalar 路径描述同步("本地手动执行,不进 CI" -> "已进 CI";sanitizer 验证仍为本地手动) ### ♻️ 重构 @@ -43,6 +45,7 @@ ### 🧪 测试 - 补 `equals` 的 AVX2 路径回归覆盖:256/512 位确定性用例(块内部分相等、跨 vec 块不等)+ 256 位随机对照参考模型。此前 `equals` 的"不等 -> 提前返回"分支仅在 ≤192 位(走 scalar 尾部)或自比(恒真)下被覆盖,AVX2 路径(≥4 字)无断言保护 +- 补 `shift_left` / `shift_right` 的 256 位随机对照测试:随机 count ∈ [0, Bits] 对照逐 bit 参考模型,覆盖此前手工用例未碰的中间 shift 值 ## [4.1.0] - 2026-07-20 diff --git a/README.md b/README.md index 90b28ab..0dd0a29 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ cmake --build build --config Release -j"$(nproc)" ctest --test-dir build --output-on-failure -C Release ``` -Scalar 路径(验证 `BITCAL_HAS_AVX2 == 0` 分支,本地手动执行,不进 CI): +Scalar 路径(验证 `BITCAL_HAS_AVX2 == 0` 分支,已进 CI;以下本地命令供调试复用): ```bash cmake -S . -B build-scalar -DCMAKE_BUILD_TYPE=Release -DBITCAL_BUILD_TESTS=ON -DBITCAL_NATIVE_ARCH=OFF -DCMAKE_CXX_FLAGS="-mno-avx2" diff --git a/tests/support/random_cases.hpp b/tests/support/random_cases.hpp index aebe8c6..d26b38c 100644 --- a/tests/support/random_cases.hpp +++ b/tests/support/random_cases.hpp @@ -67,4 +67,31 @@ std::vector> make_random_unary_cases(const std::uint64_t return cases; } +template +struct random_shift_case { + bitcal::bit_block block; + std::array words; + std::size_t count; +}; + +template +std::vector> make_random_shift_cases(const std::uint64_t seed, const std::size_t count) { + std::mt19937_64 rng(seed); + std::uniform_int_distribution count_dist(0, Bits); + std::vector> cases; + cases.reserve(count); + + for (std::size_t i = 0; i < count; ++i) { + random_shift_case tc{}; + for (std::size_t word = 0; word < Bits / 64; ++word) { + tc.words[word] = rng(); + } + tc.block = bitcal::bit_block::from_words(tc.words); + tc.count = count_dist(rng); + cases.push_back(tc); + } + + return cases; +} + } // namespace bitcal::test diff --git a/tests/support/reference_model.hpp b/tests/support/reference_model.hpp index f4f3d39..78b119c 100644 --- a/tests/support/reference_model.hpp +++ b/tests/support/reference_model.hpp @@ -87,4 +87,31 @@ bool reference_equals(const word_array& lhs, const word_array& rhs) return true; } +// Bit-by-bit reference shifts: obviously correct, O(Bits) per call. +template +word_array reference_shift_left(const word_array& words, std::size_t count) { + word_array out{}; + if (count >= Bits) + return out; + for (std::size_t i = 0; i + count < Bits; ++i) { + if ((words[i / 64] >> (i % 64)) & 1ULL) { + out[(i + count) / 64] |= 1ULL << ((i + count) % 64); + } + } + return out; +} + +template +word_array reference_shift_right(const word_array& words, std::size_t count) { + word_array out{}; + if (count >= Bits) + return out; + for (std::size_t i = count; i < Bits; ++i) { + if ((words[i / 64] >> (i % 64)) & 1ULL) { + out[(i - count) / 64] |= 1ULL << ((i - count) % 64); + } + } + return out; +} + } // namespace bitcal::test diff --git a/tests/test_bitcal.cpp b/tests/test_bitcal.cpp index b45237b..fd99020 100644 --- a/tests/test_bitcal.cpp +++ b/tests/test_bitcal.cpp @@ -778,6 +778,32 @@ bool test_ctad_bit_block_overload_runtime() { return true; } +bool test_random_shift_left_matches_reference_model_256() { + for (const auto& tc : bitcal::test::make_random_shift_cases<256>(0x51F7ULL, 64)) { + const auto actual = bitcal::shift_left<256>(tc.block.view(), tc.count); + const auto expected = bitcal::test::reference_shift_left<256>(tc.words, tc.count); + + for (std::size_t i = 0; i < bitcal::bit_block<256>::word_count; ++i) { + BITCAL_ASSERT_EQ(actual.word(i), expected[i]); + } + } + + return true; +} + +bool test_random_shift_right_matches_reference_model_256() { + for (const auto& tc : bitcal::test::make_random_shift_cases<256>(0x5B17ULL, 64)) { + const auto actual = bitcal::shift_right<256>(tc.block.view(), tc.count); + const auto expected = bitcal::test::reference_shift_right<256>(tc.words, tc.count); + + for (std::size_t i = 0; i < bitcal::bit_block<256>::word_count; ++i) { + BITCAL_ASSERT_EQ(actual.word(i), expected[i]); + } + } + + return true; +} + int main() { std::cout << "=== BitCal test suite ===" << std::endl; @@ -856,6 +882,10 @@ int main() { bitcal::test::run_case(g_counters, "test_ctad_bit_block_overload_runtime", test_ctad_bit_block_overload_runtime); bitcal::test::run_case(g_counters, "test_ctad_bit_block_overload_matches_view_form", test_ctad_bit_block_overload_matches_view_form); + bitcal::test::run_case(g_counters, "test_random_shift_left_matches_reference_model_256", + test_random_shift_left_matches_reference_model_256); + bitcal::test::run_case(g_counters, "test_random_shift_right_matches_reference_model_256", + test_random_shift_right_matches_reference_model_256); std::cout << std::endl; std::cout << "Passed: " << g_counters.pass << std::endl;