Skip to content

feat: add C benchmark harness to @stdlib/bench - #15400

Open
0PrashantYadav0 wants to merge 4 commits into
stdlib-js:developfrom
0PrashantYadav0:bench-harness
Open

0PrashantYadav0 wants to merge 4 commits into
stdlib-js:developfrom
0PrashantYadav0:bench-harness

Conversation

@0PrashantYadav0

Copy link
Copy Markdown
Member

Resolves #13480.

Description

What is the purpose of this pull request?

This pull request:

  • adds a header-only C benchmark harness (include/stdlib/bench.h) to @stdlib/bench, so C benchmark files no longer hand-roll TAP output, timing, PRNG helpers, and the length/iteration loop. The header depends only on the C standard library and emits the same TAP 13 output as the existing hand-written benchmarks.
  • adds a manifest.json (header-only, no dependencies), a C example, and a C APIs section to the @stdlib/bench README documenting every macro and helper.
  • updates tools/scripts/compile_c_benchmark to resolve the enclosing package name and pass it to make as BENCHMARK_NAME (overridable via the environment). The native C benchmark Makefile snippet turns a non-empty BENCHMARK_NAME into -DSTDLIB_BENCH_NAME="<package name>", so C benchmark names match the package-qualified names already used by JavaScript benchmarks (which read require( './../package.json' ).name). This addresses note 7 of the RFC: across files named benchmark.c there are currently 1,327 #define NAME lines but only 1,033 distinct values.
  • migrates blas/ext/base/zindex-of-truthy's C benchmark onto the harness as a pilot (200 → 105 lines). Its output name changes from # c::zindex_of_truthy:len=10 to # c::@stdlib/blas/ext/base/zindex-of-truthy:len=10.

Existing C benchmarks are untouched: their Makefiles ignore the extra BENCHMARK_NAME variable, and in Makefiles which include the new block an empty BENCHMARK_NAME yields no -D flag.

Example

#include "stdlib/blas/ext/base/zindex_of_truthy.h"
#include "stdlib/bench.h"

#ifndef STDLIB_BENCH_NAME
#define STDLIB_BENCH_NAME "zindex_of_truthy"
#endif

STDLIB_BENCHMARK( benchmark1 ) {
    int idx = -1;

    STDLIB_BENCHMARK_MALLOC_ARRAY_FLOAT64( x, len*2 );
    STDLIB_BENCHMARK_FILL_ARRAY( x, len*2, 0.0 );
    x[ (len*2)-1 ] = 1.0;

    STDLIB_BENCHMARK_LOOP_PREAMBLE {
        x[ (len*2)-4 ] = (double)( i % 4 );
        idx = stdlib_strided_zindex_of_truthy( len, (const stdlib_complex128_t *)x, 1 );
        if ( idx < 0 ) {
            printf( "unexpected result\n" );
            break;
        }
    }
    STDLIB_BENCHMARK_LOOP_EPILOGUE;
    STDLIB_BENCHMARK_FREE( x );
    STDLIB_BENCHMARK_EPILOGUE;
}

STDLIB_BENCH {
    STDLIB_BENCH_LENGTH_PREAMBLE( 3, 10000000, 1, 6 ) {
        STDLIB_BENCH_PRINT_NAME_F( "len=%d", len );
        STDLIB_RUN_BENCHMARK( benchmark1 );
    }
}

Differences from the RFC sketch

  • NAME is STDLIB_BENCH_NAME, so the build can define it without colliding with the unconditional #define NAME in existing benchmark files.
  • STDLIB_BENCH_EPILOGUE is not needed: STDLIB_BENCH generates main() around a wrapper function, so the TAP summary is printed automatically.
  • STDLIB_BENCHMARK_FILL_STRIDED_ARRAY_FLOAT64 is STDLIB_BENCHMARK_FILL_ARRAY (the sketch's signature had no stride, and assignment is type-agnostic).
  • Added STDLIB_BENCH_PREAMBLE( repeats, iterations ) for scalar benchmarks, STDLIB_BENCH_PRINT_NAME() / STDLIB_BENCH_PRINT_NAME_F( fmt, ... ) for the # c:: line (note 6 of the RFC), and STDLIB_BENCHMARK_UNUSED( x ) so scalar benchmarks compile cleanly under -Wextra.
  • The header is libc-only (note 8 of the RFC): a dependent's compile line gains a single -I and nothing is linked or compiled.

Verification

  • make examples-c EXAMPLES_FILTER=".*/@stdlib/bench/examples/c/.*" — passes.
  • make benchmark-c BENCHMARKS_FILTER=".*/blas/ext/base/zindex-of-truthy/benchmark/c/.*" — 36 benchmarks, 1..36, # ok, no compiler warnings (also clean under -Wextra).
  • BENCHMARK_NAME=custom make benchmark-c ... — override respected; compiling without the define falls back to the in-file name.
  • make benchmark-c on an unmigrated benchmark (math/base/special/abs) — unchanged behavior.
  • make lint-markdown-files FILES="lib/node_modules/@stdlib/bench/README.md" — passes.

Related Issues

Does this pull request have any related issues?

This pull request has the following related issues:

Questions

Any questions for reviewers of this pull request?

  • STDLIB_RUN_BENCHMARK keeps the name from the RFC sketch, which does not follow the STDLIB_BENCH_ / STDLIB_BENCHMARK_ prefix used by every other macro. It is documented as the one exception; happy to rename it to STDLIB_BENCHMARK_RUN if preferred.
  • The scaffold snippet tools/snippets/benchmark/c/benchmark.c is intentionally left unchanged so that adopting the harness for newly scaffolded packages can be decided separately.

Other

Any other information relevant to this pull request? This may include screenshots, references, and/or implementation notes.

  • The pre-commit shell lint (shellcheck without a severity filter) reports SC2153/SC2181 on tools/scripts/compile_c_benchmark. Nineteen of the twenty-one findings pre-exist this change; the two new ones follow the file's existing pattern (name="${NAME}" and if [[ "$?" -ne 0 ]]).
  • Mass migration of the remaining C benchmarks is left for follow-up once the harness API has settled.

Checklist

Please ensure the following tasks are completed before submitting this pull request.

AI Assistance

When authoring the changes proposed in this PR, did you use any kind of AI assistance?

  • Yes
  • No

If you answered "yes" above, how did you use AI assistance?

  • Code generation (e.g., when writing an implementation or fixing a bug)
  • Test/benchmark generation
  • Documentation (including examples)
  • Research and understanding

Disclosure

If you answered "yes" to using AI assistance, please provide a short disclosure indicating how you used AI assistance.

AI assistant reviewed the design decisions and the resulting changes, and the verification steps listed above were run locally.


@stdlib-js/reviewers

Signed-off-by: 0PrashantYadav0 <prashantyadav09783@gmail.com>
@0PrashantYadav0
0PrashantYadav0 requested a review from a team September 21, 2026 16:47
@stdlib-bot stdlib-bot added the Needs Review A pull request which needs code review. label Sep 21, 2026
@0PrashantYadav0 0PrashantYadav0 added Benchmarks Pull requests adding or improving benchmarks for measuring performance. C Issue involves or relates to C. Feature Issue or pull request for adding a new feature. labels Sep 21, 2026
Signed-off-by: 0PrashantYadav0 <prashantyadav09783@gmail.com>

---
type: pre_commit_static_analysis_report
description: Results of running static analysis checks when committing changes.
report:
  - task: lint_filenames
    status: passed
  - task: lint_editorconfig
    status: passed
  - task: lint_markdown_pkg_readmes
    status: na
  - task: lint_markdown_docs
    status: na
  - task: lint_markdown
    status: na
  - task: lint_package_json
    status: na
  - task: lint_repl_help
    status: na
  - task: lint_javascript_src
    status: na
  - task: lint_javascript_cli
    status: na
  - task: lint_javascript_examples
    status: na
  - task: lint_javascript_tests
    status: na
  - task: lint_javascript_benchmarks
    status: na
  - task: lint_python
    status: na
  - task: lint_r
    status: na
  - task: lint_c_src
    status: na
  - task: lint_c_examples
    status: na
  - task: lint_c_benchmarks
    status: na
  - task: lint_c_tests_fixtures
    status: na
  - task: lint_shell
    status: passed
  - task: lint_typescript_declarations
    status: passed
  - task: lint_typescript_tests
    status: na
  - task: lint_license_headers
    status: passed
---
*
* ## Notes
*
* - Benchmark functions always receive `len`. Scalar benchmarks which do not use it should mark it unused to keep compilation clean under `-Wextra`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should rethink this. It may make sense to just have separate macros for the two benchmark use cases (i.e., scalar and length-based benchmarks). Always passing a len variable does not seem like a good idea, even when benchmark logic doesn't need it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Split them in d97a61e:

  • STDLIB_BENCHMARK( fn )static double fn( int iterations ), run via STDLIB_RUN_BENCHMARK( fn ) inside STDLIB_BENCH_PREAMBLE( repeats, iterations )
  • STDLIB_LENGTH_BENCHMARK( fn )static double fn( int iterations, int len ), run via STDLIB_RUN_LENGTH_BENCHMARK( fn ) inside STDLIB_BENCH_LENGTH_PREAMBLE( repeats, iterations, min, max )

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

len now only exists in the length-based variants, and STDLIB_BENCHMARK_UNUSED is gone since it only existed to silence the unused len in scalar benchmarks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with STDLIB_LENGTH_BENCHMARK / STDLIB_RUN_LENGTH_BENCHMARK to mirror STDLIB_BENCH_LENGTH_PREAMBLE. Happy to rename if you'd prefer a different form (e.g., STDLIB_BENCHMARK_LENGTH).

Comment on lines +179 to +188
static void stdlib_bench_main( void ); \
static int stdlib_bench_count = 0; \
int main( void ) { \
srand( time( NULL ) ); \
stdlib_bench_print_version(); \
stdlib_bench_main(); \
stdlib_bench_print_summary( stdlib_bench_count, stdlib_bench_count ); \
return 0; \
} \
static void stdlib_bench_main( void )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear how this is working. Where is stdlib_bench_main actually implemented?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The macro ends with the signature of stdlib_bench_main and no body, so the { ... } block the author writes after STDLIB_BENCH becomes its body. I.e.,

STDLIB_BENCH {
    // ...
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expands to

static void stdlib_bench_main( void );
static int stdlib_bench_count = 0;

int main( void ) {
    srand( time( NULL ) );
    stdlib_bench_print_version();
    stdlib_bench_main();
    stdlib_bench_print_summary( stdlib_bench_count, stdlib_bench_count );
    return 0;
}

static void stdlib_bench_main( void ) {
    // ...
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The forward declaration lets the generated main call it before its body appears. That is also why there is no STDLIB_BENCH_EPILOGUE: main prints the summary after the block returns, so it cannot be forgotten. Same idea as STDLIB_NAPI_MODULE_EXPORT_FCN generating stdlib_napi_module_export_fcn_init, except the body is supplied by the caller.

I have spelled this expansion out in the header docs in d97a61e.

If you would rather avoid a generated main, the alternative is STDLIB_BENCH expanding to just int main( void ), with explicit begin/end statements inside the block (e.g., STDLIB_BENCH_PREAMBLE; ... STDLIB_BENCH_EPILOGUE;), at the cost of two lines per file which can be omitted. Let me know which you prefer.

@kgryte

kgryte commented Sep 22, 2026

Copy link
Copy Markdown
Member

A few comments:

  1. s/STDLIB_BENCH_NAME/BENCHMARK_NAME/
  2. We want both STDLIB_BENCHMARK_FILL_STRIDED_ARRAY and STDLIB_BENCHMARK_FILL_ARRAY, as the former is a generalization of the latter and there are instances where we want to fill elements according to a stride.
  3. s/STDLIB_BENCH_PRINT_NAME_F/STDLIB_BENCH_PRINT_NAME_FORMAT/

@kgryte kgryte added Needs Changes Pull request which needs changes before being merged. and removed Needs Review A pull request which needs code review. labels Sep 22, 2026
Signed-off-by: 0PrashantYadav0 <prashantyadav09783@gmail.com>

---
type: pre_commit_static_analysis_report
description: Results of running static analysis checks when committing changes.
report:
  - task: lint_filenames
    status: passed
  - task: lint_editorconfig
    status: passed
  - task: lint_markdown_pkg_readmes
    status: passed
  - task: lint_markdown_docs
    status: na
  - task: lint_markdown
    status: na
  - task: lint_package_json
    status: na
  - task: lint_repl_help
    status: na
  - task: lint_javascript_src
    status: na
  - task: lint_javascript_cli
    status: na
  - task: lint_javascript_examples
    status: na
  - task: lint_javascript_tests
    status: na
  - task: lint_javascript_benchmarks
    status: na
  - task: lint_python
    status: na
  - task: lint_r
    status: na
  - task: lint_c_src
    status: na
  - task: lint_c_examples
    status: passed
  - task: lint_c_benchmarks
    status: passed
  - task: lint_c_tests_fixtures
    status: na
  - task: lint_shell
    status: na
  - task: lint_typescript_declarations
    status: passed
  - task: lint_typescript_tests
    status: na
  - task: lint_license_headers
    status: passed
---
@0PrashantYadav0

Copy link
Copy Markdown
Member Author

@kgryte All three done in d97a61e:

  1. STDLIB_BENCH_NAME -> BENCHMARK_NAME throughout (header, README, example, the migrated benchmark, and the Makefile now passes -DBENCHMARK_NAME='"<pkg>"').
  2. Added STDLIB_BENCHMARK_FILL_STRIDED_ARRAY( x, n, stride, value ); STDLIB_BENCHMARK_FILL_ARRAY( x, n, value ) is now the unit-stride wrapper. A negative stride starts at (1-n)*stride, following the stride2offset convention. I kept both untyped, since the assignment is type-agnostic; if you would like _FLOAT64 / _FLOAT32 aliases as in the RFC sketch, those are easy to add.
  3. STDLIB_BENCH_PRINT_NAME_F -> STDLIB_BENCH_PRINT_NAME_FORMAT.

Also, per your inline comment, scalar and length-based benchmarks now use separate macros (STDLIB_BENCHMARK / STDLIB_LENGTH_BENCHMARK and STDLIB_RUN_BENCHMARK / STDLIB_RUN_LENGTH_BENCHMARK), so len is only passed where it is used.

@stdlib-bot stdlib-bot added the Needs Review A pull request which needs code review. label Sep 22, 2026
@anandkaranubc
anandkaranubc self-requested a review September 22, 2026 16:58

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Benchmarks Pull requests adding or improving benchmarks for measuring performance. C Issue involves or relates to C. Feature Issue or pull request for adding a new feature. Needs Changes Pull request which needs changes before being merged. Needs Review A pull request which needs code review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RFC]: add a C benchmark harness

3 participants