From d7de96ef9451b33e9171ef2062efb2550057c2fb Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Fri, 26 Jun 2026 10:49:53 -0400 Subject: [PATCH 1/5] Support building on ARM by providing scalar histogram fallback The metrics histogram update code unconditionally included and used __attribute__((target("avx"))) SSE/AVX intrinsics, which only exist on x86-64. This prevented the project from compiling on ARM64. Guard the x86-64 SIMD implementation and the include with an architecture check, and add a portable scalar fallback for non-x86-64 platforms (e.g., ARM64). A thin wrapper function keeps the call sites unchanged and preserves the AVX fast path on x86-64. Closes #3 --- src/metric.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/metric.cpp b/src/metric.cpp index 0c1efae..cf1b5bd 100644 --- a/src/metric.cpp +++ b/src/metric.cpp @@ -22,7 +22,9 @@ #include #include +#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) #include +#endif #include #include "arena.hpp" @@ -1398,8 +1400,10 @@ void Gauge::set(double x) { Histogram::Histogram() = default; // Vectorized histogram bucket updates with mutex protection for consistency -// AVX-optimized implementation for high performance +// AVX-optimized implementation for high performance on x86-64, scalar fallback +// on other architectures (e.g., ARM64). +#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) __attribute__((target("avx"))) static void update_histogram_buckets_simd(std::span thresholds, std::span counts, double x, @@ -1439,6 +1443,22 @@ update_histogram_buckets_simd(std::span thresholds, } } } +#endif + +static void update_histogram_buckets(std::span thresholds, + std::span counts, double x, + size_t start_idx) { +#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) + update_histogram_buckets_simd(thresholds, counts, x, start_idx); +#else + const size_t size = thresholds.size(); + for (size_t i = start_idx; i < size; ++i) { + if (x <= thresholds[i]) { + counts[i]++; + } + } +#endif +} void Histogram::observe(double x) { assert(p->thresholds.size() == p->shared.bucket_counts.size()); @@ -1459,15 +1479,15 @@ void Histogram::observe(double x) { } // Update shared directly - update_histogram_buckets_simd(p->thresholds, p->shared.bucket_counts, x, 0); + update_histogram_buckets(p->thresholds, p->shared.bucket_counts, x, 0); p->shared.sum += x; p->shared.observations++; p->mutex.unlock(); } else { // Slow path: accumulate in pending (lock-free) - update_histogram_buckets_simd(p->thresholds, p->pending.bucket_counts, x, - 0); + update_histogram_buckets(p->thresholds, p->pending.bucket_counts, x, + 0); p->pending.sum += x; p->pending.observations++; } From a377772e63f97b6023fb95d5f20274c9935f2224 Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Fri, 26 Jun 2026 11:08:03 -0400 Subject: [PATCH 2/5] Use AArch64 NEON intrinsics for histogram bucket updates Replace the scalar ARM fallback in update_histogram_buckets with a NEON implementation that processes two buckets per iteration, matching the existing AVX path. The wrapper now dispatches to the SIMD path on both x86-64 and AArch64 and falls back to scalar code on other architectures. --- src/metric.cpp | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/metric.cpp b/src/metric.cpp index cf1b5bd..f36e8e1 100644 --- a/src/metric.cpp +++ b/src/metric.cpp @@ -24,6 +24,8 @@ #if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) #include +#elif defined(__aarch64__) +#include #endif #include @@ -1400,8 +1402,8 @@ void Gauge::set(double x) { Histogram::Histogram() = default; // Vectorized histogram bucket updates with mutex protection for consistency -// AVX-optimized implementation for high performance on x86-64, scalar fallback -// on other architectures (e.g., ARM64). +// AVX-optimized implementation for high performance on x86-64, NEON-optimized +// implementation on ARM64, and a scalar fallback for other architectures. #if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) __attribute__((target("avx"))) static void @@ -1443,12 +1445,46 @@ update_histogram_buckets_simd(std::span thresholds, } } } +#elif defined(__aarch64__) +static void +update_histogram_buckets_simd(std::span thresholds, + std::span counts, double x, + size_t start_idx) { + const size_t size = thresholds.size(); + size_t i = start_idx; + + // Process 2 buckets at a time with 128-bit NEON vectors + const float64x2_t x_vec = vdupq_n_f64(x); + const uint64x2_t one = vdupq_n_u64(1); + + for (; i + 2 <= size; i += 2) { + // Compare x <= thresholds per lane; true lanes are all ones. + float64x2_t thresholds_vec = vld1q_f64(&thresholds[i]); + uint64x2_t cmp_result = vcleq_f64(x_vec, thresholds_vec); + + // Convert all-ones/all-zeros masks to per-lane 1/0 increments. + uint64x2_t increments = vandq_u64(cmp_result, one); + + // Load current counts, add increments, and store back. + uint64x2_t current_counts = vld1q_u64(&counts[i]); + uint64x2_t updated_counts = vaddq_u64(current_counts, increments); + vst1q_u64(&counts[i], updated_counts); + } + + // Handle remainder with scalar operations + for (; i < size; ++i) { + if (x <= thresholds[i]) { + counts[i]++; + } + } +} #endif static void update_histogram_buckets(std::span thresholds, std::span counts, double x, size_t start_idx) { -#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) +#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) || \ + defined(__aarch64__) update_histogram_buckets_simd(thresholds, counts, x, start_idx); #else const size_t size = thresholds.size(); From a0d64afd6f8af10a4ae123ab773bcbf65cf2f64e Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Fri, 26 Jun 2026 11:08:20 -0400 Subject: [PATCH 3/5] Fix ARM64 assembly size directive in cpu_work.cpp The GNU assembler expects `.size symbol, .-symbol`. The previous `.size spend_cpu_cycles, spend_cpu_cycles` expression is not a constant and breaks compilation on AArch64 Linux. Use the correct form so the project builds on ARM64. --- src/cpu_work.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cpu_work.cpp b/src/cpu_work.cpp index 70a8cc2..13927fb 100644 --- a/src/cpu_work.cpp +++ b/src/cpu_work.cpp @@ -55,7 +55,7 @@ asm(".text\n" " b.ne .L_loop\n" // Branch back if not zero ".L_end:\n" // End " ret\n" // Return - ".size spend_cpu_cycles, spend_cpu_cycles\n"); + ".size spend_cpu_cycles, .-spend_cpu_cycles\n"); #endif #endif From c71bdf13c4c87f4c15d0678c85c5a9043b247fb3 Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Fri, 26 Jun 2026 11:09:08 -0400 Subject: [PATCH 4/5] Align AArch64 histogram parameter formatting with project style Minor whitespace fix so the NEON function signature matches the existing AVX function's indentation. --- src/metric.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/metric.cpp b/src/metric.cpp index f36e8e1..3a870c9 100644 --- a/src/metric.cpp +++ b/src/metric.cpp @@ -1448,8 +1448,8 @@ update_histogram_buckets_simd(std::span thresholds, #elif defined(__aarch64__) static void update_histogram_buckets_simd(std::span thresholds, - std::span counts, double x, - size_t start_idx) { + std::span counts, double x, + size_t start_idx) { const size_t size = thresholds.size(); size_t i = start_idx; From 14f8552906ab6ce4ce792c3b7b7df0bfacafe0d9 Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Fri, 26 Jun 2026 11:12:10 -0400 Subject: [PATCH 5/5] Apply clang-format to ARM histogram code Pre-commit's clang-format hook reformatted the AArch64 NEON histogram bucket updates so the project style checks pass. No functional change. --- src/metric.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/metric.cpp b/src/metric.cpp index 3a870c9..39ce1e8 100644 --- a/src/metric.cpp +++ b/src/metric.cpp @@ -1446,10 +1446,9 @@ update_histogram_buckets_simd(std::span thresholds, } } #elif defined(__aarch64__) -static void -update_histogram_buckets_simd(std::span thresholds, - std::span counts, double x, - size_t start_idx) { +static void update_histogram_buckets_simd(std::span thresholds, + std::span counts, double x, + size_t start_idx) { const size_t size = thresholds.size(); size_t i = start_idx; @@ -1483,7 +1482,7 @@ update_histogram_buckets_simd(std::span thresholds, static void update_histogram_buckets(std::span thresholds, std::span counts, double x, size_t start_idx) { -#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) || \ +#if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) || \ defined(__aarch64__) update_histogram_buckets_simd(thresholds, counts, x, start_idx); #else @@ -1522,8 +1521,7 @@ void Histogram::observe(double x) { p->mutex.unlock(); } else { // Slow path: accumulate in pending (lock-free) - update_histogram_buckets(p->thresholds, p->pending.bucket_counts, x, - 0); + update_histogram_buckets(p->thresholds, p->pending.bucket_counts, x, 0); p->pending.sum += x; p->pending.observations++; }