Enhance performance of rz_bv_copy_nbits (#5541)

* Add benchmark for rz_bv_copy_nbits
* Improve performance for large to large and small to small bitvector copy
* Fix bug with nbit=64 and simplify code
* Add test for same bitvector copy
* Move bit copy logic to separate function in rz_bits.h + improve comments
* Support same vector copy for unaligned case
* Expect non-null RzTable in bench utils and add comments
* Test against reference implementation instead of hardcoded values
This commit is contained in:
Anton Angelov 2025-11-23 15:52:23 +02:00 committed by GitHub
parent f18b55f9dc
commit c186f1ec83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 555 additions and 1 deletions

View file

@ -124,6 +124,36 @@ static inline int rz_bits_leading_zeros(ut64 x) {
#endif
}
/**
* \brief Copies a bit range from \p src to \p dst at specified positions
* \param src 64-bit unsigned integer to copy bits from
* \param src_pos bit position related to \p src
* \param dst 64-bit unsigned integer to copy bits to
* \param dst_pos bit position related to \p dst
* \param size number of bits to copy (needs to be <= 64)
* \return a new 64-bit unsigned integer with the specified bit range replaced
*/
static inline ut64 rz_bits_copy_ut64(ut64 src, ut8 src_pos, ut64 dst, ut8 dst_pos, ut8 size) {
if (size >= 64) {
return src;
}
ut64 mask = ((1ull) << size) - 1;
return (dst & ~(mask << dst_pos)) | (src >> src_pos & mask) << dst_pos;
}
/**
* \brief Similar to rz_bits_copy_ut64() but for 8-bit unsigned integers
*/
static inline ut8 rz_bits_copy_ut8(ut8 src, ut8 src_pos, ut8 dst, ut8 dst_pos, ut8 size) {
if (size >= 8) {
return src;
}
ut8 mask = ((1u) << size) - 1;
return (dst & ~(mask << dst_pos)) | (src >> src_pos & mask) << dst_pos;
}
/**
* \brief Sign-extends a value from a specified bit-width to full width of type.
*

View file

@ -200,6 +200,81 @@ RZ_API ut32 rz_bv_copy(RZ_NONNULL const RzBitVector *src, RZ_NONNULL RzBitVector
return dst->_elem_len;
}
/**
* \brief Optimized version of rz_bv_copy_nbits() for large bitvectors (more than 64 bits) with bit positions aligned to BV_ELEM_SIZE
*/
static ut32 rz_bv_copy_nbits_large_aligned(const RzBitVector *src, ut32 src_start_pos, RzBitVector *dst, ut32 dst_start_pos, ut32 nbit) {
// Sanity check performed by caller
ut8 start_bits = RZ_MIN((BV_ELEM_SIZE - dst_start_pos) % BV_ELEM_SIZE, nbit);
ut8 trailing_bits = RZ_MIN((src_start_pos + nbit) % BV_ELEM_SIZE, nbit - start_bits);
ut32 middle_bytes = (nbit - start_bits) / BV_ELEM_SIZE;
ut32 src_byte = src_start_pos / BV_ELEM_SIZE;
ut32 dst_byte = dst_start_pos / BV_ELEM_SIZE;
// Handle starting bits
if (start_bits > 0) {
ut8 src_offset = src_start_pos % BV_ELEM_SIZE;
dst->bits.large_a[dst_byte] = rz_bits_copy_ut8(src->bits.large_a[src_byte], src_offset, dst->bits.large_a[dst_byte], src_offset, start_bits);
src_byte++;
dst_byte++;
}
// Handle middle bytes
if (middle_bytes > 0) {
if (src->bits.large_a == dst->bits.large_a) {
// Copy within the same vector
memmove(&dst->bits.large_a[dst_byte], &src->bits.large_a[src_byte], middle_bytes);
} else {
memcpy(&dst->bits.large_a[dst_byte], &src->bits.large_a[src_byte], middle_bytes);
}
src_byte += middle_bytes;
dst_byte += middle_bytes;
}
// Handle trailing bits
if (trailing_bits > 0) {
dst->bits.large_a[dst_byte] = rz_bits_copy_ut8(src->bits.large_a[src_byte], 0, dst->bits.large_a[dst_byte], 0, trailing_bits);
}
return nbit;
}
/**
* \brief Optimized version of rz_bv_copy_nbits() for large bitvectors (more than 64 bits) with unaligned bit positions
*/
static ut32 rz_bv_copy_nbits_large_nonaligned(const RzBitVector *src, ut32 src_start_pos, RzBitVector *dst, ut32 dst_start_pos, ut32 nbit) {
// Sanity check performed by caller
ut64 bits_remaining = nbit;
while (bits_remaining > 0) {
ut32 src_offset = src_start_pos % BV_ELEM_SIZE;
ut32 src_byte = src_start_pos / BV_ELEM_SIZE;
ut32 dst_offset = dst_start_pos % BV_ELEM_SIZE;
ut32 dst_byte = dst_start_pos / BV_ELEM_SIZE;
ut8 bits_to_write = RZ_MIN(bits_remaining, BV_ELEM_SIZE - dst_offset);
ut16 buffer;
if (src_byte < dst->_elem_len - 1 && src_offset + bits_to_write > BV_ELEM_SIZE) {
// If the bit subset spans across byte boundary, then read two bytes
buffer = src->bits.large_a[src_byte + 1] << BV_ELEM_SIZE | src->bits.large_a[src_byte];
} else {
// Otherwise 1 byte is enough
buffer = src->bits.large_a[src_byte];
}
// Extract bits from the buffer
dst->bits.large_a[dst_byte] = rz_bits_copy_ut64(buffer, src_offset, dst->bits.large_a[dst_byte], dst_offset, bits_to_write);
// Move positions
src_start_pos += bits_to_write;
dst_start_pos += bits_to_write;
bits_remaining -= bits_to_write;
}
return nbit;
}
/**
* Copy n bits from start position of source to start position of dest, return num of copied bits
* \param src RzBitVector, data source
@ -220,7 +295,33 @@ RZ_API ut32 rz_bv_copy_nbits(RZ_NONNULL const RzBitVector *src, ut32 src_start_p
return 0;
}
// normal case here
if (src->len <= 64 && dst->len <= 64) {
// Both src and dst are smaller than 64 bits
dst->bits.small_u = rz_bits_copy_ut64(src->bits.small_u, src_start_pos, dst->bits.small_u, dst_start_pos, nbit);
return nbit;
}
if (src->len > 64 && dst->len > 64) {
// Both src and dst are larger than 64 bits
if (src_start_pos % BV_ELEM_SIZE == dst_start_pos % BV_ELEM_SIZE) {
return rz_bv_copy_nbits_large_aligned(src, src_start_pos, dst, dst_start_pos, nbit);
}
if (src->bits.large_a != dst->bits.large_a) {
return rz_bv_copy_nbits_large_nonaligned(src, src_start_pos, dst, dst_start_pos, nbit);
}
// Use a temporary bitvector for same-vector copies
RzBitVector *temp = rz_bv_new(rz_bv_len(dst));
rz_bv_copy(dst, temp);
ut32 bits_copied = rz_bv_copy_nbits_large_nonaligned(src, src_start_pos, temp, dst_start_pos, nbit);
rz_bv_copy(temp, dst);
rz_bv_free(temp);
return bits_copied;
}
// Only one of the bitvectors is large
// TODO: add specialized functions for large to small and small to large bit copy
for (ut32 i = 0; i < nbit; ++i) {
bool c = rz_bv_get(src, src_start_pos + i);
rz_bv_set(dst, dst_start_pos + i, c);

View file

@ -44,5 +44,6 @@ option('install_sigdb', type: 'boolean', value: false, description: 'Downloads a
option('debugger', type: 'boolean', value: true)
option('enable_tests', type: 'boolean', value: true, description: 'Build unit tests in test/unit')
option('enable_benchmarks', type: 'boolean', value: false, description: 'Build micro-benchmarks in test/bench')
option('enable_rz_test', type: 'boolean', value: true, description: 'Build rz-test executable for regression testing')
option('regenerate_cmds', type: 'feature', value: 'auto', description: 'Regenerate the cmd_descs.[ch] files (requires PyYAML)')

View file

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2025 Anton Angelov <anton.angelov@protonmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#include "bench_utils.h"
#include <rz_util.h>
/**
* \file bench_bitvector.c
* \brief Benchmark for `rz_bv_*` functions (currently `rz_bv_copy_nbits` only)
*
* Tests various copy scenarios to measure performance of optimized paths
*/
static void bench_bv_copy_small_60_bit(RzTable *t_out) {
RzBitVector *src = rz_bv_new_from_ut64(64, 0x1122334455667788ULL);
RzBitVector *dst = rz_bv_new_from_ut64(64, 0);
RZ_BENCH_RUN("rz_bv_copy_nbits: small to small (60 bit)", t_out, 1000000, {
rz_bv_copy_nbits(src, 2, dst, 2, 60);
});
rz_bv_free(src);
rz_bv_free(dst);
}
static void bench_bv_copy_large_100_bit_aligned(RzTable *t_out) {
RzBitVector *src = rz_bv_new_from_ut64(128, 0);
RzBitVector *dst = rz_bv_new_from_ut64(128, 0);
for (int i = 0; i < src->len; i++) {
rz_bv_set(src, i, i % 2 == 0 ? true : false);
}
RZ_BENCH_RUN("rz_bv_copy_nbits: large to large (100 bit) aligned", t_out, 1000000, {
rz_bv_copy_nbits(src, 0, dst, 0, 100);
});
rz_bv_free(src);
rz_bv_free(dst);
}
static void bench_bv_copy_large_100_bit_unaligned(RzTable *t_out) {
RzBitVector *src = rz_bv_new_from_ut64(128, 0);
RzBitVector *dst = rz_bv_new_from_ut64(128, 0);
for (int i = 0; i < src->len; i++) {
rz_bv_set(src, i, i % 2 == 0 ? true : false);
}
RZ_BENCH_RUN("rz_bv_copy_nbits: large to large (100 bit) unaligned", t_out, 1000000, {
rz_bv_copy_nbits(src, 2, dst, 3, 100);
});
rz_bv_free(src);
rz_bv_free(dst);
}
static void bench_bv_copy_small_to_large_60_bit(RzTable *t_out) {
RzBitVector *src = rz_bv_new_from_ut64(64, 0x1122334455667788ULL);
RzBitVector *dst = rz_bv_new_from_ut64(128, 0);
RZ_BENCH_RUN("rz_bv_copy_nbits: small to large (60 bit)", t_out, 1000000, {
rz_bv_copy_nbits(src, 2, dst, 3, 60);
});
rz_bv_free(src);
rz_bv_free(dst);
}
static void bench_bv_copy_large_to_small_60_bit(RzTable *t_out) {
RzBitVector *src = rz_bv_new_from_ut64(128, 0x1122334455667788ULL);
RzBitVector *dst = rz_bv_new_from_ut64(64, 0);
RZ_BENCH_RUN("rz_bv_copy_nbits: large to small (60 bit)", t_out, 1000000, {
rz_bv_copy_nbits(src, 2, dst, 3, 60);
});
rz_bv_free(src);
rz_bv_free(dst);
}
int main(RzTable *t_out) {
RzTable *t = rz_table_new();
rz_table_set_columnsf(t, "snnnn", "Benchmark", "Iterations", "Total time [ms]", "Average time [us/op]", "Throughput [ops/sec]");
// Micro benchmarks
bench_bv_copy_small_60_bit(t);
bench_bv_copy_large_100_bit_aligned(t);
bench_bv_copy_large_100_bit_unaligned(t);
bench_bv_copy_large_to_small_60_bit(t);
bench_bv_copy_small_to_large_60_bit(t);
// Print results
const char *out = rz_table_tostring(t);
printf("%s\n", out);
free(out);
rz_table_free(t);
return 0;
}

45
test/bench/bench_utils.c Normal file
View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2025 Anton Angelov <anton.angelov@protonmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#include "bench_utils.h"
#include <stdio.h>
/**
* \brief Initialize benchmark context
*/
RZ_API void rz_bench_init(RZ_NONNULL RzBenchCtx *ctx, RZ_NONNULL const char *name, ut64 iterations) {
rz_return_if_fail(ctx && name);
ctx->name = name;
ctx->iterations = iterations;
ctx->start_time = 0;
ctx->total_time = 0;
}
/**
* \brief Start timing a benchmark
*/
RZ_API void rz_bench_start(RZ_NONNULL RzBenchCtx *ctx) {
rz_return_if_fail(ctx);
ctx->start_time = rz_time_now_mono();
}
/**
* \brief End timing a benchmark
*/
RZ_API void rz_bench_end(RZ_NONNULL RzBenchCtx *ctx) {
rz_return_if_fail(ctx);
ctx->total_time = rz_time_now_mono() - ctx->start_time;
}
/**
* \brief Print benchmark results
*/
RZ_API void rz_bench_report(RZ_NONNULL RzBenchCtx *ctx, RZ_NONNULL RzTable *t) {
rz_return_if_fail(ctx && t);
double total_ms = ctx->total_time / 1000.0;
double avg_us = ctx->iterations != 0 ? (double)ctx->total_time / ctx->iterations : 0;
double ops_per_sec = ctx->total_time != 0 ? (ctx->iterations * 1000000.0) / ctx->total_time : 0;
rz_table_add_rowf(t, "sdfff", ctx->name, (int)ctx->iterations, total_ms, avg_us, ops_per_sec);
}

48
test/bench/bench_utils.h Normal file
View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2025 Anton Angelov <anton.angelov@protonmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#ifndef BENCH_UTILS_H
#define BENCH_UTILS_H
#include <rz_types.h>
#include <rz_util.h>
/**
* \brief Description of the state of a micro benchmark
*/
typedef struct rz_bench_ctx_t {
const char *name; ///< name of a micro benchmark
ut64 iterations; ///< number of iterations
ut64 start_time; ///< start time of the benchmark in microseconds
ut64 total_time; ///< total elapsed time of the benchmark in microseconds
} RzBenchCtx;
RZ_API void rz_bench_init(RZ_NONNULL RzBenchCtx *ctx, RZ_NONNULL const char *name, ut64 iterations);
RZ_API void rz_bench_start(RZ_NONNULL RzBenchCtx *ctx);
RZ_API void rz_bench_end(RZ_NONNULL RzBenchCtx *ctx);
RZ_API void rz_bench_report(RZ_NONNULL RzBenchCtx *ctx, RZ_NONNULL RzTable *t);
/**
* \brief Run a benchmark with the given code block
*
* Example usage:
* \code
* RZ_BENCH_RUN("my_function", table, 1000000, {
* my_function(data);
* });
* \endcode
*/
#define RZ_BENCH_RUN(name, table, iterations, code) \
do { \
RzBenchCtx ctx; \
rz_bench_init(&ctx, name, iterations); \
rz_bench_start(&ctx); \
for (ut64 i = 0; i < iterations; i++) { \
code; \
} \
rz_bench_end(&ctx); \
rz_bench_report(&ctx, table); \
} while (0)
#endif // BENCH_UTILS_H

41
test/bench/meson.build Normal file
View file

@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: 2025
# SPDX-License-Identifier: LGPL-3.0-only
if get_option('enable_benchmarks')
# Build utility library for benchmarks
bench_utils = static_library('bench_utils',
'bench_utils.c',
include_directories: [platform_inc],
dependencies: [rz_util_dep],
)
bench_utils_dep = declare_dependency(
link_with: bench_utils,
include_directories: [include_directories('.')],
)
# List of benchmarks to build
benchmarks = [
'bitvector',
]
# Create benchmark executables
foreach bench : benchmarks
exe = executable('bench_@0@'.format(bench),
'bench_@0@.c'.format(bench),
include_directories: [platform_inc, '.'],
dependencies: [
bench_utils_dep,
rz_util_dep,
rz_core_dep,
],
install: false,
implicit_include_directories: false,
)
# Register with meson test system (run with: meson test --benchmark)
benchmark(bench, exe, suite: 'bench', timeout: 120)
endforeach
message('Benchmarks enabled - build with "meson compile" and run with "meson test --benchmark"')
endif

View file

@ -1,2 +1,6 @@
subdir('unit')
subdir('integration')
if get_option('enable_benchmarks')
subdir('bench')
endif

View file

@ -59,10 +59,20 @@ bool test_rz_bits_spread(void) {
mu_end;
}
bool test_rz_bits_copy(void) {
mu_assert_eq(rz_bits_copy_ut64(0x1122334455667788, 24, 0x8877665544332211, 8, 16), 0x8877665544445511, "Incorrect bit copy");
mu_assert_eq(rz_bits_copy_ut64(0x1122334455667788, 0, 0x0, 1, 63), 0x22446688aaccef10, "Incorrect bit copy");
mu_assert_eq(rz_bits_copy_ut64(0x1122334455667788, 0, 0x8877665544332211, 0, 64), 0x1122334455667788, "Incorrect bit copy");
mu_assert_eq(rz_bits_copy_ut8(0xAB, 0, 0xCD, 0, 8), 0xAB, "Incorrect bit copy");
mu_end;
}
bool all_tests() {
mu_run_test(test_rz_bits_count);
mu_run_test(test_rz_bits_spread);
mu_run_test(test_rz_bits_trailing_zero);
mu_run_test(test_rz_bits_copy);
return tests_passed != tests_run;
}

View file

@ -1164,6 +1164,177 @@ bool test_rz_bv_copy_nbits(void) {
rz_bv_free(too_small);
rz_bv_free(a);
rz_bv_free(b);
mu_end;
}
/**
* \brief Reference implementation of rz_bv_copy_nbits() to test against
*/
static ut32 rz_bv_copy_nbits_ref(const RzBitVector *src, ut32 src_start_pos, RzBitVector *dst, ut32 dst_start_pos, ut32 nbit) {
rz_return_val_if_fail(src && dst, 0);
ut32 max_nbit = RZ_MIN((src->len - src_start_pos), (dst->len - dst_start_pos));
// prevent overflow
if (max_nbit < nbit) {
return 0;
}
for (ut32 i = 0; i < nbit; ++i) {
rz_bv_set(dst, dst_start_pos + i, rz_bv_get(src, src_start_pos + i));
}
return nbit;
}
/**
* \brief Performs rz_bv_copy_nbits() with actual and reference implementation and compares results
*/
static const char *test_rz_bv_copy_nbits_against_ref(const RzBitVector *src, ut32 src_pos, RzBitVector *dst, ut32 dst_pos, ut32 nbit) {
RzBitVector *src_copy = rz_bv_new(rz_bv_len(src));
RzBitVector *dst_copy = rz_bv_new(rz_bv_len(dst));
RzBitVector *dst_copy_ref = rz_bv_new(rz_bv_len(dst));
const char *error = NULL;
rz_bv_copy(src, src_copy);
rz_bv_copy(dst, dst_copy);
rz_bv_copy(dst, dst_copy_ref);
if (rz_bv_copy_nbits(src_copy, src_pos, dst_copy, dst_pos, nbit) != nbit) {
error = "rz_bv_copy_nbits() incorrect return";
goto finally;
}
if (rz_bv_copy_nbits_ref(src_copy, src_pos, dst_copy_ref, dst_pos, nbit) != nbit) {
error = "rz_bv_copy_nbits_ref() incorrect result";
goto finally;
}
if (rz_bv_cmp(dst_copy, dst_copy_ref)) {
error = "rz_bv_copy_nbits() result differs from reference";
goto finally;
}
// Test with inverted src/dst for extra certainty
rz_bv_copy(dst, dst_copy);
rz_bv_toggle_all(src_copy);
rz_bv_toggle_all(dst_copy);
if (rz_bv_copy_nbits(src_copy, src_pos, dst_copy, dst_pos, nbit) != nbit) {
error = "rz_bv_copy_nbits() incorrect result";
goto finally;
}
rz_bv_toggle_all(dst_copy);
if (rz_bv_cmp(dst_copy, dst_copy_ref)) {
error = "rz_bv_copy_nbits() result differs from reference";
goto finally;
}
finally:
rz_bv_free(src_copy);
rz_bv_free(dst_copy);
rz_bv_free(dst_copy_ref);
return error;
}
bool test_rz_bv_copy_nbits_small(void) {
RzBitVector *a = rz_bv_new_from_ut64(64, 0x67452301);
RzBitVector *b = rz_bv_new_from_ut64(64, 0x0);
const char *error;
error = test_rz_bv_copy_nbits_against_ref(a, 1, b, 2, 62);
mu_assert_null(error, error);
error = test_rz_bv_copy_nbits_against_ref(a, 0, b, 63, 1);
mu_assert_null(error, error);
rz_bv_free(a);
rz_bv_free(b);
mu_end;
}
bool test_rz_bv_copy_nbits_large_aligned(void) {
RzBitVector *a = rz_bv_new(128);
RzBitVector *b = rz_bv_new_from_ut64(128, 0x0);
const char *error;
rz_bv_set_all(a, true);
/// copy same offset at same byte
error = test_rz_bv_copy_nbits_against_ref(a, 5, b, 5, 3);
mu_assert_null(error, error);
/// copy with front/end byte trailing bits, but no middle bytes
error = test_rz_bv_copy_nbits_against_ref(a, 3, b, 3, 7);
mu_assert_null(error, error);
/// copy with front and end trailing bits and middle bytes
error = test_rz_bv_copy_nbits_against_ref(a, 3, b, 3, 16);
mu_assert_null(error, error);
/// copy without front/trailing bits
error = test_rz_bv_copy_nbits_against_ref(a, 8, b, 8, 32);
mu_assert_null(error, error);
/// copy 1 bit
error = test_rz_bv_copy_nbits_against_ref(a, 13, b, 13, 1);
mu_assert_null(error, error);
/// copy all except 1 bit
error = test_rz_bv_copy_nbits_against_ref(a, 1, b, 1, 127);
mu_assert_null(error, error);
/// Copy bits within the same bitvector
rz_bv_set_from_ut64(b, 0xAAAABBBBCCCCDDDD);
ut32 actual_copy = rz_bv_copy_nbits(b, 8, b, 16, 32);
mu_assert_eq(actual_copy, 32, "copy 32 bits");
mu_assert_streq_free(rz_bv_as_hex_string(b, false), "0xaaaabbccccdddddd", "copy large aligned");
rz_bv_free(a);
rz_bv_free(b);
mu_end;
}
bool test_rz_bv_copy_nbits_large_unaligned(void) {
RzBitVector *a = rz_bv_new(128);
RzBitVector *b = rz_bv_new(128);
const char *error;
rz_bv_set_all(a, true);
/// copy different offset but same byte
error = test_rz_bv_copy_nbits_against_ref(a, 5, b, 2, 20);
mu_assert_null(error, error);
/// copy different offset and different byte
error = test_rz_bv_copy_nbits_against_ref(a, 10, b, 20, 10);
mu_assert_null(error, error);
/// copy at bit boundary
error = test_rz_bv_copy_nbits_against_ref(a, 48, b, 1, 22);
mu_assert_null(error, error);
/// copy 1 bit
error = test_rz_bv_copy_nbits_against_ref(a, 55, b, 13, 1);
mu_assert_null(error, error);
/// copy all except 1 bit
error = test_rz_bv_copy_nbits_against_ref(a, 0, b, 1, 127);
mu_assert_null(error, error);
/// Copy bits within the same bitvector
rz_bv_set_from_ut64(b, 0xAAAABBBBCCCCDDDD);
ut32 actual_copy = rz_bv_copy_nbits(b, 0, b, 2, 30);
mu_assert_eq(actual_copy, 30, "copy 30 bits");
mu_assert_streq_free(rz_bv_as_hex_string(b, false), "0xaaaabbbb33337775", "copy large aligned");
rz_bv_free(a);
rz_bv_free(b);
mu_end;
}
@ -1284,6 +1455,9 @@ bool all_tests() {
mu_run_test(test_rz_bv_set_operations);
mu_run_test(test_rz_bv_set_to_bytes_le);
mu_run_test(test_rz_bv_copy_nbits);
mu_run_test(test_rz_bv_copy_nbits_small);
mu_run_test(test_rz_bv_copy_nbits_large_aligned);
mu_run_test(test_rz_bv_copy_nbits_large_unaligned);
mu_run_test(test_rz_bv_extra_operations);
return tests_passed != tests_run;