-
Notifications
You must be signed in to change notification settings - Fork 11
U256 limb-native arithmetic: beat Rust on UniswapV2 (87ns -> 20ns) #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8eeced0
Fix u256 benchmark fairness: correct sqrt_price construction and add …
koko1123 7d0573c
U256 limb-native arithmetic: beat Rust on UniswapV2 (87ns -> 20ns)
koko1123 98b5c71
Drop zbench, add criterion-style harness: 20/26 wins vs alloy.rs
koko1123 f88277e
Update bench/RESULTS.md
koko1123 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| /// u256-only benchmark: eth.zig vs alloy.rs (ruint) | ||
| /// | ||
| /// All test values match bench/u256_bench.zig exactly. | ||
| /// alloy's U256 is ruint::Uint<256, 4> -- native [u64; 4] limb arithmetic. | ||
|
|
||
| use alloy_primitives::{U256, Uint}; | ||
| use criterion::{black_box, criterion_group, criterion_main, Criterion}; | ||
|
|
||
| type U512 = Uint<512, 8>; | ||
|
|
||
| // ================================================================ | ||
| // Test values -- identical to u256_bench.zig | ||
| // ================================================================ | ||
|
|
||
| const ONE_ETH: U256 = U256::from_limbs([1_000_000_000_000_000_000u64, 0, 0, 0]); | ||
|
|
||
| // 100 ETH = 100_000_000_000_000_000_000 = 0x56BC75E2D63100000 | ||
| const RESERVE_IN: U256 = U256::from_limbs([0x6BC75E2D63100000, 5, 0, 0]); | ||
| const RESERVE_OUT: U256 = U256::from_limbs([200_000_000_000u64, 0, 0, 0]); | ||
|
|
||
| // 2^96 = 79228162514264337593543950336 | ||
| const SQRT_PRICE: U256 = U256::from_limbs([0, 0x100000000, 0, 0]); | ||
| const AMOUNT_IN_SMALL: U256 = U256::from_limbs([1_000_000_000_000_000u64, 0, 0, 0]); | ||
|
|
||
| // Full-width 256-bit values | ||
| const FULL_A: U256 = U256::from_limbs([ | ||
| 0x12345678_9ABCDEF0, | ||
| 0xDEADBEEF_CAFEBABE, | ||
| 0x12345678_9ABCDEF0, | ||
| 0xDEADBEEF_CAFEBABE, | ||
| ]); | ||
| const FULL_B: U256 = U256::from_limbs([ | ||
| 0xDEADBEEF_CAFEBABE, | ||
| 0x12345678_9ABCDEF0, | ||
| 0xDEADBEEF_CAFEBABE, | ||
| 0x12345678_9ABCDEF0, | ||
| ]); | ||
| const FULL_C: U256 = U256::from_limbs([ | ||
| 0x00000000_00000001, | ||
| 0x00000000_00000000, | ||
| 0x00000000_00000000, | ||
| 0x00000001_00000000, | ||
| ]); | ||
|
|
||
| // ================================================================ | ||
| // Benchmarks | ||
| // ================================================================ | ||
|
|
||
| fn bench_u256(c: &mut Criterion) { | ||
| let mut group = c.benchmark_group("u256"); | ||
|
|
||
| // --- Primitives --- | ||
|
|
||
| group.bench_function("add", |b| { | ||
| let a = ONE_ETH; | ||
| let b_val = U256::from(997_000_000_000_000_000u64); | ||
| b.iter(|| { | ||
| let result = black_box(a).wrapping_add(black_box(b_val)); | ||
| black_box(result); | ||
| }) | ||
| }); | ||
|
|
||
| group.bench_function("mul_small", |b| { | ||
| let a = ONE_ETH; | ||
| b.iter(|| { | ||
| let result = black_box(a).wrapping_mul(U256::from(997u64)); | ||
| black_box(result); | ||
| }) | ||
| }); | ||
|
|
||
| group.bench_function("mul_full", |b| { | ||
| b.iter(|| { | ||
| let result = black_box(FULL_A).wrapping_mul(black_box(FULL_B)); | ||
| black_box(result); | ||
| }) | ||
| }); | ||
|
|
||
| group.bench_function("div_small", |b| { | ||
| let large = U256::from(997_000_000_000_000_000_000u128); | ||
| b.iter(|| { | ||
| let result = black_box(large).checked_div(black_box(ONE_ETH)); | ||
| black_box(result); | ||
| }) | ||
| }); | ||
|
|
||
| group.bench_function("div_full", |b| { | ||
| b.iter(|| { | ||
| let result = black_box(FULL_A).checked_div(black_box(FULL_C)); | ||
| black_box(result); | ||
| }) | ||
| }); | ||
|
|
||
| // --- UniswapV2 getAmountOut (naive: step-by-step u256 arithmetic) --- | ||
| // Both Zig and Rust do the exact same formula with wrapping u256 ops. | ||
| // This is the fair apples-to-apples comparison. | ||
|
|
||
| group.bench_function("uniswapv2_naive", |b| { | ||
| let amount_in = ONE_ETH; | ||
| let reserve_in = RESERVE_IN; | ||
| let reserve_out = RESERVE_OUT; | ||
| b.iter(|| { | ||
| let amount_in_with_fee = black_box(amount_in).wrapping_mul(U256::from(997u64)); | ||
| let numerator = amount_in_with_fee.wrapping_mul(black_box(reserve_out)); | ||
| let denominator = | ||
| black_box(reserve_in).wrapping_mul(U256::from(1000u64)).wrapping_add(amount_in_with_fee); | ||
| let amount_out = numerator / denominator; | ||
| black_box(amount_out); | ||
| }) | ||
| }); | ||
|
|
||
| // --- mulDiv: (a * b) / c with true 512-bit intermediate --- | ||
|
|
||
| group.bench_function("mulDiv", |b| { | ||
| let liquidity = ONE_ETH; | ||
| let sqrt_price = SQRT_PRICE; | ||
| let denom = ONE_ETH.wrapping_add(U256::from(1_000_000u64)); | ||
| b.iter(|| { | ||
| let a = U512::from(black_box(liquidity)); | ||
| let b_val = U512::from(black_box(sqrt_price)); | ||
| let d = U512::from(black_box(denom)); | ||
| let result = U256::from((a * b_val) / d); | ||
| black_box(result); | ||
| }) | ||
| }); | ||
|
|
||
| // --- UniswapV4 getNextSqrtPriceFromAmount0RoundingUp --- | ||
| // product = amount_in * sqrt_price (u256, no overflow for these values) | ||
| // denominator = liquidity + product | ||
| // next_sqrt_price = (liquidity * sqrt_price) / denominator (via U512) | ||
|
|
||
| group.bench_function("uniswapv4_swap", |b| { | ||
| let liquidity = ONE_ETH; | ||
| let sqrt_price = SQRT_PRICE; | ||
| let amount_in = AMOUNT_IN_SMALL; | ||
| b.iter(|| { | ||
| let product = black_box(amount_in).wrapping_mul(black_box(sqrt_price)); | ||
| let denominator = black_box(liquidity).wrapping_add(product); | ||
| let num = U512::from(black_box(liquidity)) * U512::from(black_box(sqrt_price)); | ||
| let next_sqrt_price = U256::from(num / U512::from(denominator)); | ||
| black_box(next_sqrt_price); | ||
| }) | ||
| }); | ||
|
|
||
| group.finish(); | ||
| } | ||
|
|
||
| criterion_group!(benches, bench_u256); | ||
| criterion_main!(benches); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| ROOT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| ALLOY_DIR="$SCRIPT_DIR/alloy-bench" | ||
|
|
||
| command -v zig >/dev/null 2>&1 || { echo "ERROR: zig not found"; exit 1; } | ||
| command -v cargo >/dev/null 2>&1 || { echo "ERROR: cargo not found"; exit 1; } | ||
| command -v python3 >/dev/null 2>&1 || { echo "ERROR: python3 not found"; exit 1; } | ||
|
|
||
| echo "" | ||
| echo "================================================================" | ||
| echo " u256 Benchmark: eth.zig vs alloy.rs (ruint)" | ||
| echo "================================================================" | ||
| echo "" | ||
|
|
||
| # -- Step 1: eth-zig u256 benchmarks -- | ||
| echo "[1/3] Running eth-zig u256 benchmarks (ReleaseFast)..." | ||
| ZIG_OUTPUT=$(cd "$ROOT_DIR" && zig build bench-u256 2>&1) | ||
| echo "$ZIG_OUTPUT" | grep -v "^BENCH_JSON" | ||
| echo "" | ||
|
|
||
| # -- Step 2: alloy.rs u256 benchmarks -- | ||
| echo "[2/3] Running alloy.rs u256 benchmarks (cargo bench --release)..." | ||
| RUST_OUTPUT=$(cd "$ALLOY_DIR" && cargo bench --bench u256_comparison 2>&1) | ||
| echo " Done." | ||
| echo "" | ||
|
|
||
| # -- Step 3: Compare -- | ||
| echo "[3/3] Comparing results..." | ||
| echo "" | ||
|
|
||
| python3 - "$ZIG_OUTPUT" "$RUST_OUTPUT" << 'PYTHON_SCRIPT' | ||
|
koko1123 marked this conversation as resolved.
|
||
| import sys | ||
| import json | ||
| import re | ||
|
|
||
| zig_output = sys.argv[1] | ||
| rust_output = sys.argv[2] | ||
|
|
||
| def parse_ns(value_str, unit_str): | ||
| v = float(value_str) | ||
| if unit_str == 'ns': | ||
| return round(v) | ||
| elif unit_str in ('us', 'µs'): | ||
| return round(v * 1000) | ||
| elif unit_str == 'ms': | ||
| return round(v * 1_000_000) | ||
| return round(v) | ||
|
|
||
| # Parse BENCH_JSON lines from Zig output | ||
| zig_ns = {} | ||
| for line in zig_output.split('\n'): | ||
| if line.startswith('BENCH_JSON|'): | ||
| try: | ||
| data = json.loads(line[len('BENCH_JSON|'):]) | ||
| zig_ns[data['name']] = data['ns_per_op'] | ||
| except (json.JSONDecodeError, KeyError): | ||
| pass | ||
|
|
||
| # Parse criterion output | ||
| alloy_ns = {} | ||
| for line in rust_output.split('\n'): | ||
| m = re.match(r'^([a-zA-Z0-9_]+/[a-zA-Z0-9_]+)\s+time:\s+\[[\d.]+ \w+\s+([\d.]+)\s+(ns|µs|ms)', line.strip()) | ||
| if m: | ||
| alloy_ns[m.group(1)] = parse_ns(m.group(2), m.group(3)) | ||
|
|
||
| # Name mapping: zig -> criterion | ||
| name_map = { | ||
| 'u256_add': 'u256/add', | ||
| 'u256_mul_small': 'u256/mul_small', | ||
| 'u256_mul_full': 'u256/mul_full', | ||
| 'u256_div_small': 'u256/div_small', | ||
| 'u256_div_full': 'u256/div_full', | ||
| 'u256_uniswapv2_naive': 'u256/uniswapv2_naive', | ||
| 'u256_mulDiv': 'u256/mulDiv', | ||
| 'u256_uniswapv4_swap': 'u256/uniswapv4_swap', | ||
| } | ||
|
|
||
| bench_order = [ | ||
| 'u256_add', | ||
| 'u256_mul_small', | ||
| 'u256_mul_full', | ||
| 'u256_div_small', | ||
| 'u256_div_full', | ||
| 'u256_uniswapv2_naive', | ||
| 'u256_mulDiv', | ||
| 'u256_uniswapv4_swap', | ||
| ] | ||
|
|
||
| GREEN = '\033[0;32m' | ||
| RED = '\033[0;31m' | ||
| YELLOW = '\033[0;33m' | ||
| BOLD = '\033[1m' | ||
| NC = '\033[0m' | ||
|
|
||
| print(f"\n{BOLD}=== Apples-to-apples: same formula, same u256 ops ==={NC}\n") | ||
| print(f"{BOLD}{'Benchmark':<28} {'eth-zig':>10} {'alloy.rs':>10} {'Result':>20}{NC}") | ||
| print(f"{'-'*28} {'-'*10} {'-'*10} {'-'*20}") | ||
|
|
||
| zig_wins = 0 | ||
| alloy_wins = 0 | ||
| ties = 0 | ||
| total = 0 | ||
|
|
||
| for zig_name in bench_order: | ||
| alloy_name = name_map.get(zig_name, '') | ||
| z = zig_ns.get(zig_name) | ||
| a = alloy_ns.get(alloy_name) | ||
|
|
||
| if z is not None and a is not None: | ||
| total += 1 | ||
| if z == a or (z > 0 and a > 0 and abs(z - a) / max(z, a) < 0.1): | ||
| label = 'tie' | ||
| color = NC | ||
| ties += 1 | ||
| elif z < a: | ||
| ratio = a / z if z > 0 else 999.99 | ||
| label = f'zig {ratio:.2f}x' | ||
| color = GREEN | ||
| zig_wins += 1 | ||
| else: | ||
| ratio = z / a if a > 0 else 999.99 | ||
| label = f'rs {ratio:.2f}x' | ||
| color = RED | ||
| alloy_wins += 1 | ||
| print(f"{zig_name:<28} {z:>7} ns {a:>7} ns {color}{label:>20}{NC}") | ||
| elif z is not None: | ||
| print(f"{zig_name:<28} {z:>7} ns {'---':>10} {'(zig only)':>20}") | ||
|
|
||
| print(f"\n{BOLD}{'='*28} {'='*10} {'='*10} {'='*20}{NC}") | ||
| print(f"\n{BOLD}Score: eth-zig {zig_wins}/{total} | alloy.rs {alloy_wins}/{total} | tied {ties}/{total}{NC}") | ||
|
|
||
| # Show zig-only optimized benchmark | ||
| z_opt = zig_ns.get('u256_uniswapv2_optimized') | ||
| z_naive = zig_ns.get('u256_uniswapv2_naive') | ||
| a_naive = alloy_ns.get('u256/uniswapv2_naive') | ||
|
|
||
| if z_opt is not None: | ||
| print(f"\n{BOLD}=== eth.zig compound limb optimization ==={NC}\n") | ||
| print(f"{'u256_uniswapv2_optimized':<28} {z_opt:>7} ns (stays in [4]u64 limb space)") | ||
| if z_naive is not None and z_naive > 0: | ||
| print(f"{'u256_uniswapv2_naive':<28} {z_naive:>7} ns (step-by-step u256, same as alloy)") | ||
| speedup = z_naive / z_opt if z_opt > 0 else 0 | ||
| print(f"{'Optimization speedup':<28} {YELLOW}{speedup:.2f}x{NC}") | ||
| if a_naive is not None and z_opt > 0: | ||
| vs_rust = a_naive / z_opt | ||
| if vs_rust >= 1: | ||
| print(f"{'vs alloy.rs naive':<28} {GREEN}{vs_rust:.2f}x faster{NC}") | ||
| else: | ||
| print(f"{'vs alloy.rs naive':<28} {RED}{1/vs_rust:.2f}x slower{NC}") | ||
|
|
||
| print() | ||
| PYTHON_SCRIPT | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.