Examples
Real-world benchmark examples showing poly-bench patterns and best practices
A minimal benchmark suite comparing a single operation across Go, TypeScript, and Rust. This is the starting point for most benchmarks.
1suite basic {2 description: "Basic benchmark example"3 warmup: 1004 compare: true5 baseline: "go"6
7 setup go {8 helpers {9 func add(a, b int) int {10 return a + b11 }12 }13 }14
15 setup ts {16 helpers {17 function add(a: number, b: number): number {18 return a + b19 }20 }21 }22
23 setup rust {24 helpers {25 fn add(a: i32, b: i32) -> i32 {26 a + b27 }28 }29 }30
31 bench addition {32 go: add(1, 2)33 ts: add(1, 2)34 rust: add(1, 2)35 }36}compare: true and baseline to see relative performance. This makes it easy to understand which implementation is faster at a glance.A real-world cryptographic benchmark comparing Keccak256 implementations. This pattern is common when evaluating crypto libraries across languages.
1use std::charting2
3suite keccak {4 description: "Keccak256 hash benchmark"5 warmup: 1006 compare: true7 baseline: "go"8 mode: "auto"9 targetTime: 3000ms10 count: 311
12 setup go {13 import (14 "golang.org/x/crypto/sha3"15 )16
17 helpers {18 func keccak256Go(data []byte) []byte {19 h := sha3.NewLegacyKeccak256()20 h.Write(data)21 return h.Sum(nil)22 }23 }24 }25
26 setup ts {27 import {28 import { keccak256 } from 'viem'29 }30
31 helpers {32 function keccak256Ts(data: Uint8Array): Uint8Array {33 return keccak256(data, 'bytes')34 }35 }36 }37
38 setup rust {39 import {40 use tiny_keccak::{Hasher, Keccak};41 }42
43 helpers {44 fn keccak256_rust(data: &[u8]) -> [u8; 32] {45 let mut hasher = Keccak::v256();46 let mut output = [0u8; 32];47 hasher.update(data);48 hasher.finalize(&mut output);49 output50 }51 }52 }53
54 fixture data {55 hex: "68656c6c6f20776f726c64"56 }57
58 bench keccak256Bench {59 go: keccak256Go(data)60 ts: keccak256Ts(data)61 rust: keccak256_rust(&data)62 }63
64 after {65 charting.drawTable(66 title: "Keccak256 Performance",67 output: "keccak-bar.svg",68 xlabel: "Implementation"69 )70 }71}Group related benchmarks together to share setup code. This is useful for comparing different input sizes or variations of an algorithm.
1suite hashBenchmarks {2 description: "Hash function benchmarks with varying input sizes"3 warmup: 504 compare: true5 baseline: "go"6 mode: "auto"7 targetTime: 2000ms8
9 setup go {10 import (11 "golang.org/x/crypto/sha3"12 )13
14 helpers {15 func keccak256(data []byte) []byte {16 h := sha3.NewLegacyKeccak256()17 h.Write(data)18 return h.Sum(nil)19 }20 }21 }22
23 setup ts {24 import {25 import { keccak256 } from 'viem'26 }27 }28
29 setup rust {30 import {31 use tiny_keccak::{Hasher, Keccak};32 }33
34 helpers {35 fn keccak256(data: &[u8]) -> [u8; 32] {36 let mut hasher = Keccak::v256();37 let mut output = [0u8; 32];38 hasher.update(data);39 hasher.finalize(&mut output);40 output41 }42 }43 }44
45 # Small input (32 bytes)46 fixture smallData {47 hex: "68656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c64"48 }49
50 # Medium input (256 bytes)51 fixture mediumData {52 hex: "68656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c6468656c6c6f20776f726c64"53 }54
55 bench hashSmall {56 go: keccak256(smallData)57 ts: keccak256(smallData)58 rust: keccak256(&smallData)59 }60
61 bench hashMedium {62 go: keccak256(mediumData)63 ts: keccak256(mediumData)64 rust: keccak256(&mediumData)65 }66}bench blocks.The std::anvil module spawns a local Anvil node for Ethereum RPC benchmarks. This is useful for comparing blockchain client libraries.
1use std::anvil2
3suite rpcBenchmarks {4 description: "Ethereum RPC benchmarks against local Anvil"5 warmup: 106 compare: true7 baseline: "go"8 mode: "auto"9 targetTime: 5000ms10
11 globalSetup {12 anvil.spawnAnvil(fork: "https://eth.llamarpc.com")13 }14
15 setup go {16 import (17 "context"18 "github.com/ethereum/go-ethereum/ethclient"19 )20
21 declare {22 var client *ethclient.Client23 var ctx context.Context24 }25
26 init {27 ctx = context.Background()28 client, _ = ethclient.Dial(os.Getenv("ANVIL_RPC_URL"))29 }30
31 helpers {32 func getBlockNumber() uint64 {33 num, _ := client.BlockNumber(ctx)34 return num35 }36 }37 }38
39 setup ts {40 import {41 import { createPublicClient, http } from 'viem'42 import { mainnet } from 'viem/chains'43 }44
45 declare {46 let client: any47 }48
49 init {50 client = createPublicClient({51 chain: mainnet,52 transport: http(ANVIL_RPC_URL),53 })54 }55
56 helpers {57 async function getBlockNumber(): Promise<bigint> {58 return await client.getBlockNumber()59 }60 }61 }62
63 bench getBlockNumber {64 go: getBlockNumber()65 ts: await getBlockNumber()66 }67}ANVIL_RPC_URL value is available after calling anvil.spawnAnvil() (injected directly for TypeScript and available via env var for Go/Rust).Compare ABI encoding performance across different libraries. This is a common benchmark for Ethereum developer tooling.
1use std::charting2
3suite abiBenchmarks {4 description: "ABI encoding/decoding benchmarks"5 warmup: 1006 compare: true7 baseline: "go"8 mode: "auto"9 targetTime: 2000ms10 count: 311
12 setup go {13 import (14 "math/big"15 "github.com/ethereum/go-ethereum/accounts/abi"16 "github.com/ethereum/go-ethereum/common"17 )18
19 declare {20 var args abi.Arguments21 }22
23 init {24 uint256Type, _ := abi.NewType("uint256", "", nil)25 addressType, _ := abi.NewType("address", "", nil)26 args = abi.Arguments{27 {Type: uint256Type},28 {Type: addressType},29 }30 }31
32 helpers {33 func encodeArgs(amount *big.Int, addr common.Address) []byte {34 data, _ := args.Pack(amount, addr)35 return data36 }37 }38 }39
40 setup ts {41 import {42 import { encodeAbiParameters, parseAbiParameters } from 'viem'43 }44
45 declare {46 const params = parseAbiParameters('uint256, address')47 }48
49 helpers {50 function encodeArgs(amount: bigint, addr: string): string {51 return encodeAbiParameters(params, [amount, addr])52 }53 }54 }55
56 setup rust {57 import {58 use alloy_sol_types::{sol, SolValue};59 use alloy_primitives::{U256, Address, address};60 }61
62 helpers {63 fn encode_args(amount: U256, addr: Address) -> Vec<u8> {64 (amount, addr).abi_encode()65 }66 }67 }68
69 fixture testAmount {70 hex: "0000000000000000000000000000000000000000000000000de0b6b3a7640000"71 }72
73 fixture testAddress {74 hex: "d8da6bf26964af9d7eed9e03e53415d37aa96045"75 }76
77 bench encodeSimple {78 go: encodeArgs(big.NewInt(1000000000000000000), common.HexToAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))79 ts: encodeArgs(1000000000000000000n, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")80 rust: encode_args(U256::from(1000000000000000000u64), address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))81 }82
83 after {84 charting.drawTable(85 title: "ABI Encoding Performance",86 xlabel: "Implementation"87 )88 }89}Enable memory profiling to track allocations alongside timing. This helps identify not just which implementation is fastest, but which is most memory-efficient.
1suite memoryBenchmarks {2 description: "Benchmarks with memory profiling"3 warmup: 504 compare: true5 baseline: "go"6 memory: true # Enable memory profiling7
8 setup go {9 helpers {10 func allocateSlice(n int) []byte {11 data := make([]byte, n)12 for i := range data {13 data[i] = byte(i % 256)14 }15 return data16 }17 }18 }19
20 setup ts {21 helpers {22 function allocateArray(n: number): Uint8Array {23 const data = new Uint8Array(n)24 for (let i = 0; i < n; i++) {25 data[i] = i % 25626 }27 return data28 }29 }30 }31
32 setup rust {33 helpers {34 fn allocate_vec(n: usize) -> Vec<u8> {35 let mut data = Vec::with_capacity(n);36 for i in 0..n {37 data.push((i % 256) as u8);38 }39 data40 }41 }42 }43
44 bench allocate1KB {45 go: allocateSlice(1024)46 ts: allocateArray(1024)47 rust: allocate_vec(1024)48 }49
50 bench allocate1MB {51 go: allocateSlice(1048576)52 ts: allocateArray(1048576)53 rust: allocate_vec(1048576)54 }55}Memory results appear in the output:
1Running: allocate1KB2 go: 156 ns/op | 1,024 B/op, 1 allocs/op3 ts: 892 ns/op | 1,024 B/op4 rust: 98 ns/op | 1,024 B/op, 1 allocs/opUse before, after, and each hooks for setup and teardown. This is useful for benchmarks that need shared state or cleanup.
1suite withHooks {2 description: "Demonstrating lifecycle hooks"3 warmup: 504 compare: true5 baseline: "go"6
7 setup go {8 declare {9 var counter int10 }11
12 helpers {13 func increment() int {14 counter++15 return counter16 }17 func reset() {18 counter = 019 }20 }21 }22
23 setup ts {24 declare {25 let counter = 026 }27
28 helpers {29 function increment(): number {30 return ++counter31 }32 function reset(): void {33 counter = 034 }35 }36 }37
38 setup rust {39 helpers {40 static mut COUNTER: i32 = 0;41 fn increment() -> i32 {42 unsafe {43 COUNTER += 1;44 COUNTER45 }46 }47 fn reset() {48 unsafe { COUNTER = 0; }49 }50 }51 }52
53 bench incrementBench {54 # before runs once before the timed loop for each language55 before go: reset()56 before ts: reset()57
58 # each runs before every iteration, outside the timing window59 each go: reset()60 each ts: reset()61
62 go: increment()63 ts: increment()64 rust: increment()65
66 # after runs once after the timed loop67 after go: { _ = counter }68 after ts: { void counter }69 }70}Generate multiple chart types for visual comparison. The charting module supports tables, speedup charts, speedup charts, and tables.
1use std::charting2
3suite chartExamples {4 description: "Benchmark with multiple chart outputs"5 warmup: 1006 compare: true7 baseline: "go"8 mode: "auto"9 targetTime: 2000ms10
11 setup go {12 helpers {13 func compute(n int) int {14 sum := 015 for i := 0; i < n; i++ {16 sum += i17 }18 return sum19 }20 }21 }22
23 setup ts {24 helpers {25 function compute(n: number): number {26 let sum = 027 for (let i = 0; i < n; i++) {28 sum += i29 }30 return sum31 }32 }33 }34
35 setup rust {36 helpers {37 fn compute(n: i32) -> i32 {38 (0..n).sum()39 }40 }41 }42
43 bench compute100 {44 go: compute(100)45 ts: compute(100)46 rust: compute(100)47 }48
49 bench compute1000 {50 go: compute(1000)51 ts: compute(1000)52 rust: compute(1000)53 }54
55 bench compute10000 {56 go: compute(10000)57 ts: compute(10000)58 rust: compute(10000)59 }60
61 after {62 # Bar chart comparing all implementations63 charting.drawTable(64 title: "Compute Performance by Size",65 output: "compute-bar.svg",66 xlabel: "Benchmark"67 )68
69 # Line chart showing scaling behavior70 charting.drawSpeedupChart(71 title: "Scaling Behavior",72 output: "compute-line.svg",73 xlabel: "Input Size"74 )75
76 # Speedup chart relative to Go77 charting.drawSpeedupChart(78 title: "Speedup vs Go",79 output: "compute-speedup.svg"80 )81 }82}--output results/ when running to specify where charts should be saved.You don't have to benchmark all three languages. poly-bench works with any subset.
1suite goVsTs {2 description: "Comparing only Go and TypeScript"3 warmup: 1004 compare: true5 baseline: "go"6
7 setup go {8 import (9 "encoding/json"10 )11
12 helpers {13 func parseJSON(data []byte) map[string]any {14 var result map[string]any15 json.Unmarshal(data, &result)16 return result17 }18 }19 }20
21 setup ts {22 helpers {23 function parseJSON(data: Uint8Array): object {24 return JSON.parse(new TextDecoder().decode(data))25 }26 }27 }28
29 # No setup rust block — Rust won't be benchmarked30
31 # JSON payload encoded as UTF-8 hex: {"name":"test","value":42}32 fixture jsonData {33 hex: "7b226e616d65223a2274657374222c2276616c7565223a34327d"34 }35
36 bench parseJSON {37 go: parseJSON(jsonData)38 ts: parseJSON(jsonData)39 # No rust: line — skipped for this benchmark40 }41}Even with all three languages defined, you can run only a subset:
$# Run all languages$poly-bench run suite.bench
$# Run only Go$poly-bench run suite.bench --lang go
$# Run Go and Rust only$poly-bench run suite.bench --lang go --lang rust
$# Run TypeScript only$poly-bench run suite.bench --lang tsstd::anvil, std::charting, std::constants