This is an automated email from the ASF dual-hosted git repository.

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new 5f0741f2 Rewrite RISC-V Zvbc CRC32C with adaptive VLEN and pure-vector 
folding (#3571)
5f0741f2 is described below

commit 5f0741f23d6ce0b3770ba00463caa0d4ca4fb13f
Author: Felix-Gong <[email protected]>
AuthorDate: Mon Oct 12 00:26:01 2026 +0800

    Rewrite RISC-V Zvbc CRC32C with adaptive VLEN and pure-vector folding 
(#3571)
    
    PR3332 introduced a Zvbc vector CRC32C path, but its implementation
    kept per-lane scalar round-trips (vector->scalar xor->vector) in the
    hot loop and fixed VLEN=128 (vsetvl_e64m1(2)), so VLEN>=256 cores
    only used half the width. On Spacemit X100 the Zvbc path measured
    ~450-600 MB/s, slower than the scalar Zbc path (~1.5 GB/s), making
    the "Zvbc preferred" dispatch a ~2.5x regression there.
    
    Rewrite rv_crc32c_vclmul:
    
      - adaptive VLEN: one e64m1 pair at VLEN>=256 (4 lanes) or two
        pairs at VLEN=128 (4 lanes carried on 2 vectors), so 128-bit
        cores run the vector path at full width;
      - pure-vector main loop: vlseg2e64 de-interleaved loads and
        vclmul_vx broadcast constants (k1..k4), zero scalar extraction;
      - all-vector reduction: single-element vector vclmul for the
        4-to-1 lane merge and Barrett reduction, removing scalar-clmul
        dependencies (safe on cores with Zvbc but no scalar Zbc, e.g.
        SG2044);
      - same fold constants and math as the scalar core -> bit-exact.
    
    Correctness on k3: RFC3720 vectors (4 standard PASS), Extend
    composition, 70 boundary/misalign/large combos, and the official
    CRC.* gtest suite (5/5 PASSED) all bit-consistent across
    table/clmul/vclmul.
    
    Performance on k3 (X100, VLEN=256, 100-round median, same harness as
    PR3312):
    
        len      PR3332 vclmul (MB/s)   this (MB/s)   speedup
        1 KiB           578                 5895         10.2x
        4 KiB           590                 9400         15.9x
        64 KiB          600                11502         19.2x
        1 MiB           508                11708         23.1x
    
    Signed-off-by: Xiaofei Gong <[email protected]>
    Signed-off-by: YuanSheng <[email protected]>
---
 src/butil/crc32c.cc | 252 ++++++++++++++++++++++++----------------------------
 1 file changed, 118 insertions(+), 134 deletions(-)

diff --git a/src/butil/crc32c.cc b/src/butil/crc32c.cc
index 4b942eea..ac81c7f6 100644
--- a/src/butil/crc32c.cc
+++ b/src/butil/crc32c.cc
@@ -612,175 +612,159 @@ static bool is_zbc() {
 
 #if defined(__riscv_zvbc)
 // Hardware-accelerated CRC32C using RISC-V Zvbc vector carry-less 
multiplication.
-// Uses RVV vclmul/vclmulh to process 2 lanes per vector operation (VLEN=128).
-// With VLEN=128, each vector register holds 2 x 64-bit elements.
-// 4 lanes are processed using 2 vector register pairs per clmul step.
+// Adaptive VLEN: the fold keeps the exact 4-lane / 64-byte span of the scalar
+// clmul core (same constants) and carries the four lanes on `segs` e64m1
+// vector pairs (vl=4 on VLEN>=256, two vl=2 pairs on VLEN=128), so 128-bit
+// cores run the vector path at full width.  Every carry-less multiply goes
+// through the vector vclmul path: cores with Zvbc but without scalar Zbc
+// (e.g. SG2044) must never execute a scalar `clmul`.  Bit-exact with the
+// table/bitwise fallback.
+static inline uint64_t rv_vclmul1(uint64_t a, uint64_t b, bool hi) {
+  const size_t vl1 = 1;
+  vuint64m1_t va = __riscv_vmv_v_x_u64m1(a, vl1);
+  vuint64m1_t vb = __riscv_vmv_v_x_u64m1(b, vl1);
+  return __riscv_vmv_x_s_u64m1_u64(hi ? __riscv_vclmulh_vv_u64m1(va, vb, vl1)
+                                      : __riscv_vclmul_vv_u64m1(va, vb, vl1));
+}
 static uint32_t rv_crc32c_vclmul(uint32_t crc, const char* buf, size_t len) {
   crc ^= 0xFFFFFFFF;
 
   const uint8_t* p = reinterpret_cast<const uint8_t*>(buf);
   size_t n = len;
 
-  if (n < 64) {
+  size_t max_vl = __riscv_vsetvlmax_e64m1();
+  const size_t vl = max_vl >= 4 ? 4 : (max_vl >= 2 ? 2 : 0);
+  const uint64_t chunk = 64;
+
+  if (n < chunk || vl == 0) {
     return rv_crc32c_bitwise(crc, p, n) ^ 0xFFFFFFFF;
   }
 
-  // Align to 16-byte boundary
-  uintptr_t mis = (uintptr_t)p & 0xF;
+  // Align to 16-byte boundary (same bitwise walk as the scalar core).
+  uintptr_t mis = reinterpret_cast<uintptr_t>(p) & 0xF;
   if (mis) {
     size_t pre = 16 - mis;
     if (pre > n) pre = n;
     crc = rv_crc32c_bitwise(crc, p, pre);
     p += pre;
     n -= pre;
-    if (n < 64) {
+    if (n < chunk) {
       return rv_crc32c_bitwise(crc, p, n) ^ 0xFFFFFFFF;
     }
   }
 
-  // Set up RVV for 64-bit elements: vl = min(VLEN/64, 2) = 2 for VLEN=128
-  // If VLEN < 128, vl will be 1 and the vector path cannot be used; fall back.
-  size_t vl = __riscv_vsetvl_e64m1(2);
-  if (vl < 2) {
-    return rv_crc32c_bitwise(crc, p, n) ^ 0xFFFFFFFF;
+  // Four lanes, carried by `segs` e64m1 vector pairs: 1 pair (vl=4) or 2
+  // pairs (vl=2).  RVV types cannot live in arrays, so named variables.
+  const uint32_t segs = vl == 2 ? 2 : 1;
+  const size_t seg_bytes = vl * 16;
+
+  vuint64m1_t lo_a, hi_a, lo_b, hi_b;
+  {
+    vuint64m1x2_t sg = __riscv_vlseg2e64_v_u64m1x2(
+        reinterpret_cast<const uint64_t*>(p), vl);
+    lo_a = __riscv_vget_v_u64m1x2_u64m1(sg, 0);
+    hi_a = __riscv_vget_v_u64m1x2_u64m1(sg, 1);
   }
+  if (segs == 2) {
+    vuint64m1x2_t sg = __riscv_vlseg2e64_v_u64m1x2(
+        reinterpret_cast<const uint64_t*>(p + seg_bytes), vl);
+    lo_b = __riscv_vget_v_u64m1x2_u64m1(sg, 0);
+    hi_b = __riscv_vget_v_u64m1x2_u64m1(sg, 1);
+  }
+  if (vl == 2) {
+    uint64_t l0[2], h0[2];
+    __riscv_vse64_v_u64m1(l0, lo_a, vl);
+    __riscv_vse64_v_u64m1(h0, hi_a, vl);
+    l0[0] ^= (uint64_t)crc;
+    lo_a = __riscv_vle64_v_u64m1(l0, vl);
+    hi_a = __riscv_vle64_v_u64m1(h0, vl);
+  } else {
+    uint64_t l0[4];
+    __riscv_vse64_v_u64m1(l0, lo_a, vl);
+    l0[0] ^= (uint64_t)crc;
+    lo_a = __riscv_vle64_v_u64m1(l0, vl);
+  }
+  p += chunk;
+  n -= chunk;
 
-  // Construct fold constant vectors: {k1, k2} and {k3, k4}
-  // Each element gets the appropriate constant for its position:
-  // element 0 (lo half) uses k1/k3, element 1 (hi half) uses k2/k4
-  uint64_t k12_arr[2] = { crc32c_fold_const[0], crc32c_fold_const[1] };
-  uint64_t k34_arr[2] = { crc32c_fold_const[2], crc32c_fold_const[3] };
-  vuint64m1_t k12_vec = __riscv_vle64_v_u64m1(k12_arr, vl);  // {k1, k2}
-  vuint64m1_t k34_vec = __riscv_vle64_v_u64m1(k34_arr, vl);  // {k3, k4}
-
-  // Load first 64 bytes into 4 vector registers.
-  // Each vector = one 128-bit lane: {lo_64, hi_64}
-  // Use memcpy to avoid strict-aliasing violations when loading uint8_t* as 
uint64_t*
-  uint64_t lane1_buf[2], lane2_buf[2], lane3_buf[2], lane4_buf[2];
-  memcpy(lane1_buf, p + 0,  16);
-  memcpy(lane2_buf, p + 16, 16);
-  memcpy(lane3_buf, p + 32, 16);
-  memcpy(lane4_buf, p + 48, 16);
-  vuint64m1_t lane1 = __riscv_vle64_v_u64m1(lane1_buf, vl);
-  vuint64m1_t lane2 = __riscv_vle64_v_u64m1(lane2_buf, vl);
-  vuint64m1_t lane3 = __riscv_vle64_v_u64m1(lane3_buf, vl);
-  vuint64m1_t lane4 = __riscv_vle64_v_u64m1(lane4_buf, vl);
-
-  // XOR CRC into element 0 of first lane
-  uint64_t tmp[2];
-  __riscv_vse64_v_u64m1(tmp, lane1, vl);
-  tmp[0] ^= (uint64_t)crc;
-  lane1 = __riscv_vle64_v_u64m1(tmp, vl);
+  const uint64_t k1 = crc32c_fold_const[0];
+  const uint64_t k2 = crc32c_fold_const[1];
 
-  p += 64;
-  n -= 64;
+  while (n >= chunk) {
+    vuint64m1x2_t sg = __riscv_vlseg2e64_v_u64m1x2(
+        reinterpret_cast<const uint64_t*>(p), vl);
+    vuint64m1_t dl = __riscv_vget_v_u64m1x2_u64m1(sg, 0);
+    vuint64m1_t dh = __riscv_vget_v_u64m1x2_u64m1(sg, 1);
+    vuint64m1_t lv = __riscv_vxor_vv_u64m1(
+        __riscv_vclmul_vx_u64m1(lo_a, k1, vl),
+        __riscv_vclmul_vx_u64m1(hi_a, k2, vl), vl);
+    vuint64m1_t hv = __riscv_vxor_vv_u64m1(
+        __riscv_vclmulh_vx_u64m1(lo_a, k1, vl),
+        __riscv_vclmulh_vx_u64m1(hi_a, k2, vl), vl);
+    lo_a = __riscv_vxor_vv_u64m1(lv, dl, vl);
+    hi_a = __riscv_vxor_vv_u64m1(hv, dh, vl);
+    if (segs == 2) {
+      vuint64m1x2_t sg2 = __riscv_vlseg2e64_v_u64m1x2(
+          reinterpret_cast<const uint64_t*>(p + seg_bytes), vl);
+      vuint64m1_t dl2 = __riscv_vget_v_u64m1x2_u64m1(sg2, 0);
+      vuint64m1_t dh2 = __riscv_vget_v_u64m1x2_u64m1(sg2, 1);
+      vuint64m1_t lv2 = __riscv_vxor_vv_u64m1(
+          __riscv_vclmul_vx_u64m1(lo_b, k1, vl),
+          __riscv_vclmul_vx_u64m1(hi_b, k2, vl), vl);
+      vuint64m1_t hv2 = __riscv_vxor_vv_u64m1(
+          __riscv_vclmulh_vx_u64m1(lo_b, k1, vl),
+          __riscv_vclmulh_vx_u64m1(hi_b, k2, vl), vl);
+      lo_b = __riscv_vxor_vv_u64m1(lv2, dl2, vl);
+      hi_b = __riscv_vxor_vv_u64m1(hv2, dh2, vl);
+    }
+    p += chunk;
+    n -= chunk;
+  }
 
-  // Main loop: fold 64 bytes per iteration using vector carry-less multiply.
-  //
-  // For each 128-bit lane {lo, hi}, the fold computes:
-  //   new_lo = clmul(lo, k1) ^ clmul(hi, k2) ^ data_lo
-  //   new_hi = clmulh(lo, k1) ^ clmulh(hi, k2) ^ data_hi
-  //
-  // With k12_vec = {k1, k2} and element-wise vclmul:
-  //   vclmul(lane, k12_vec)  = {clmul(lo, k1), clmul(hi, k2)}   (lo halves of 
products)
-  //   vclmulh(lane, k12_vec) = {clmulh(lo, k1), clmulh(hi, k2)} (hi halves of 
products)
-  //
-  // The 128-bit XOR of (lo*k1) and (hi*k2) decomposes element-wise:
-  //   new_lo = clmul(lo,k1) ^ clmul(hi,k2) = vclmul[0] ^ vclmul[1]
-  //   new_hi = clmulh(lo,k1) ^ clmulh(hi,k2) = vclmulh[0] ^ vclmulh[1]
-  //
-  // So we need to XOR across elements. With VLEN=128 (2 elements), we use
-  // scalar extraction for the cross-element XOR since there's no vector
-  // permute instruction for just 2 elements that's more efficient.
-  while (n >= 64) {
-    uint64_t d1_buf[2], d2_buf[2], d3_buf[2], d4_buf[2];
-    memcpy(d1_buf, p + 0,  16);
-    memcpy(d2_buf, p + 16, 16);
-    memcpy(d3_buf, p + 32, 16);
-    memcpy(d4_buf, p + 48, 16);
-    vuint64m1_t d1 = __riscv_vle64_v_u64m1(d1_buf, vl);
-    vuint64m1_t d2 = __riscv_vle64_v_u64m1(d2_buf, vl);
-    vuint64m1_t d3 = __riscv_vle64_v_u64m1(d3_buf, vl);
-    vuint64m1_t d4 = __riscv_vle64_v_u64m1(d4_buf, vl);
-
-    // Fold each lane using vector clmul with {k1, k2}
-    uint64_t lo_r[2], hi_r[2], d_r[2];
-
-    // Lane 1
-    __riscv_vse64_v_u64m1(lo_r, __riscv_vclmul_vv_u64m1(lane1, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(hi_r, __riscv_vclmulh_vv_u64m1(lane1, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(d_r, d1, vl);
-    d_r[0] ^= lo_r[0] ^ lo_r[1];
-    d_r[1] ^= hi_r[0] ^ hi_r[1];
-    lane1 = __riscv_vle64_v_u64m1(d_r, vl);
-
-    // Lane 2
-    __riscv_vse64_v_u64m1(lo_r, __riscv_vclmul_vv_u64m1(lane2, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(hi_r, __riscv_vclmulh_vv_u64m1(lane2, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(d_r, d2, vl);
-    d_r[0] ^= lo_r[0] ^ lo_r[1];
-    d_r[1] ^= hi_r[0] ^ hi_r[1];
-    lane2 = __riscv_vle64_v_u64m1(d_r, vl);
-
-    // Lane 3
-    __riscv_vse64_v_u64m1(lo_r, __riscv_vclmul_vv_u64m1(lane3, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(hi_r, __riscv_vclmulh_vv_u64m1(lane3, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(d_r, d3, vl);
-    d_r[0] ^= lo_r[0] ^ lo_r[1];
-    d_r[1] ^= hi_r[0] ^ hi_r[1];
-    lane3 = __riscv_vle64_v_u64m1(d_r, vl);
-
-    // Lane 4
-    __riscv_vse64_v_u64m1(lo_r, __riscv_vclmul_vv_u64m1(lane4, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(hi_r, __riscv_vclmulh_vv_u64m1(lane4, k12_vec, vl), 
vl);
-    __riscv_vse64_v_u64m1(d_r, d4, vl);
-    d_r[0] ^= lo_r[0] ^ lo_r[1];
-    d_r[1] ^= hi_r[0] ^ hi_r[1];
-    lane4 = __riscv_vle64_v_u64m1(d_r, vl);
+  uint64_t loa[4], hia[4];
+  __riscv_vse64_v_u64m1(loa, lo_a, vl);
+  __riscv_vse64_v_u64m1(hia, hi_a, vl);
+  if (segs == 2) {
+    __riscv_vse64_v_u64m1(loa + 2, lo_b, vl);
+    __riscv_vse64_v_u64m1(hia + 2, hi_b, vl);
+  }
 
-    p += 64;
-    n -= 64;
+  // Merge the four lanes with k3/k4 via single-element vector CLMUL.
+  const uint64_t k3 = crc32c_fold_const[2];
+  const uint64_t k4 = crc32c_fold_const[3];
+  uint64_t x0 = loa[0], x1 = hia[0];
+  for (uint32_t j = 1; j < 4; ++j) {
+    const size_t vl1 = 1;
+    vuint64m1_t vlo = __riscv_vmv_v_x_u64m1(x0, vl1);
+    vuint64m1_t vhi = __riscv_vmv_v_x_u64m1(x1, vl1);
+    vuint64m1_t vk0 = __riscv_vmv_v_x_u64m1(k3, vl1);
+    vuint64m1_t vk1 = __riscv_vmv_v_x_u64m1(k4, vl1);
+    vuint64m1_t l = __riscv_vxor_vv_u64m1(
+        __riscv_vclmul_vv_u64m1(vlo, vk0, vl1),
+        __riscv_vclmul_vv_u64m1(vhi, vk1, vl1), vl1);
+    vuint64m1_t h = __riscv_vxor_vv_u64m1(
+        __riscv_vclmulh_vv_u64m1(vlo, vk0, vl1),
+        __riscv_vclmulh_vv_u64m1(vhi, vk1, vl1), vl1);
+    x0 = __riscv_vmv_x_s_u64m1_u64(l) ^ loa[j];
+    x1 = __riscv_vmv_x_s_u64m1_u64(h) ^ hia[j];
   }
 
-  // Reduce 4 lanes to 1 using {k3, k4}
-  // Same fold pattern: fold lane_a into lane_b
-  #define FOLD_INTO(dst, src) do { \
-    uint64_t _lo[2], _hi[2], _d[2]; \
-    __riscv_vse64_v_u64m1(_lo, __riscv_vclmul_vv_u64m1(src, k34_vec, vl), vl); 
\
-    __riscv_vse64_v_u64m1(_hi, __riscv_vclmulh_vv_u64m1(src, k34_vec, vl), 
vl); \
-    __riscv_vse64_v_u64m1(_d, dst, vl); \
-    _d[0] ^= _lo[0] ^ _lo[1]; \
-    _d[1] ^= _hi[0] ^ _hi[1]; \
-    dst = __riscv_vle64_v_u64m1(_d, vl); \
-  } while(0)
-
-  FOLD_INTO(lane2, lane1);  // lane2 = fold(lane1) ^ lane2
-  FOLD_INTO(lane3, lane2);  // lane3 = fold(lane2) ^ lane3
-  FOLD_INTO(lane4, lane3);  // lane4 = fold(lane3) ^ lane4
-  #undef FOLD_INTO
-
-  // Extract final 128-bit state from vector register
-  uint64_t final_state[2];
-  __riscv_vse64_v_u64m1(final_state, lane4, vl);
-  uint64_t x0 = final_state[0];
-  uint64_t x1 = final_state[1];
-
-  // Barrett reduction: 128-bit -> 32-bit CRC (scalar)
-  uint64_t t4 = rv_clmul(x0, RV_CRC32C_CONST_1);
-  uint64_t t3 = rv_clmulh(x0, RV_CRC32C_CONST_1);
+  // Barrett reduction with single-element vector vclmul (no scalar Zbc).
+  uint64_t t4 = rv_vclmul1(x0, RV_CRC32C_CONST_1, false);
+  uint64_t t3 = rv_vclmul1(x0, RV_CRC32C_CONST_1, true);
   uint64_t t1 = x1 ^ t4;
   t4 = t1 & RV_CRC32_MASK32;
   t1 >>= 32;
-  uint64_t t0 = rv_clmul(t4, RV_CRC32C_CONST_0);
+  uint64_t t0 = rv_vclmul1(t4, RV_CRC32C_CONST_0, false);
   t3 = (t3 << 32) ^ t1 ^ t0;
 
   t4 = t3 & RV_CRC32_MASK32;
-  t4 = rv_clmul(t4, RV_CRC32C_CONST_QUO);
+  t4 = rv_vclmul1(t4, RV_CRC32C_CONST_QUO, false);
   t4 &= RV_CRC32_MASK32;
-  t4 = rv_clmul(t4, RV_CRC32C_CONST_POLY);
+  t4 = rv_vclmul1(t4, RV_CRC32C_CONST_POLY, false);
   t4 ^= t3;
 
-  uint32_t c = (uint32_t)((t4 >> 32) & RV_CRC32_MASK32);
+  uint32_t c = static_cast<uint32_t>((t4 >> 32) & RV_CRC32_MASK32);
   if (n) {
     c = rv_crc32c_bitwise(c, p, n);
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to