Skip to content

Instantly share code, notes, and snippets.

@xomiachuna
Created March 18, 2025 13:54
Show Gist options
  • Select an option

  • Save xomiachuna/a00c5e625dc744508e50468b22a83a33 to your computer and use it in GitHub Desktop.

Select an option

Save xomiachuna/a00c5e625dc744508e50468b22a83a33 to your computer and use it in GitHub Desktop.
Benchmarking if-else vs switch case vs map lookup in golang

Benchmarking if-else vs switch-case vs map lookup for string for string lookup in golang

The python script in this gist provides a way to generate functions that do simple string lookup using if-else, switch-case, map[string] (allocated both inside the function and pre-allocated).

Requirements

go >= 1.24.1 # for b.Loop() in benchmarks
python3.10 # for generating the golang files

Running

See python3 generate_files --help for the available options. In order to generate a set of files that compares the performance of the techniques above for num_cases=500 cases with strings being string_legths=300 characters long and run the generated benchmarks:

$ python3 generate_files.py --dir . --num_cases 500 --string_lengths 300 && go test -v -bench . ./...
Created go.mod
Created main.go
Created main_test.go
goos: linux
goarch: amd64
pkg: benchmarking-matching
cpu: Intel(R) Celeron(R) CPU N3350 @ 1.10GHz
BenchmarkMatchIfElse_500_300
BenchmarkMatchIfElse_500_300-2            178699              6204 ns/op
BenchmarkMatchSwitchCase_500_300
BenchmarkMatchSwitchCase_500_300-2      40564178                27.96 ns/op
BenchmarkMatchMap_500_300
BenchmarkMatchMap_500_300-2                15735             76073 ns/op
BenchmarkMatchMapPrealloc_500_300
BenchmarkMatchMapPrealloc_500_300-2     14435722                83.90 ns/op
PASS
ok      benchmarking-matching   4.671s
from argparse import ArgumentParser
from pathlib import Path
import random
import string
from textwrap import dedent
def random_string(length: int) -> str:
return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(length))
def generate_if_else_func(name: str, n: int, string_length: int) -> str:
header = dedent(
f"""
func {name}(expected string) int {{
res := 0
"""
)
body = " " * 4
cases = sorted(set(random_string(string_length) for _ in range(n)))
for i, test_case in enumerate(cases):
body += f'''if expected == "{test_case}" {{ res = {random.randint(0, 2 ** 16)} }}'''
if not i == len(cases) - 1:
body += " else\n "
footer = dedent(
f"""
return res
}}
"""
)
return header + body + footer
def generate_preallocated_map_func(name: str, n: int, string_length: int) -> str:
map_header = dedent(
f"""
var {name}_lookup = map[string]int {{
"""
)
cases = sorted(set(random_string(string_length) for _ in range(n)))
map_body = "".join(
f' "{test_case}": {random.randint(0, 2 ** 16)},\n' for i, test_case in enumerate(cases)
)
map_footer = dedent(
"""
}
"""
)
header = dedent(
f"""
func {name}(expected string) int {{
"""
)
footer = dedent(
f"""
if res, found := {name}_lookup[expected]; found {{
return res
}}
return 0
}}
"""
)
return map_header + map_body + map_footer + header + footer
def generate_map_func(name: str, n: int, string_length: int) -> str:
header = dedent(
f"""
func {name}(expected string) int {{
lookup := map[string]int{{
"""
)
cases = sorted(set(random_string(string_length) for _ in range(n)))
body = "".join(
f' "{test_case}": {random.randint(0, 2 ** 16)},\n' for i, test_case in enumerate(cases)
)
footer = dedent(
f"""
}}
if res, found := lookup[expected]; found {{
return res
}}
return 0
}}
"""
)
return header + body + footer
def generate_switch_case_func(name: str, n: int, string_length: int) -> str:
header = dedent(
f"""
func {name}(expected string) int {{
switch expected {{
"""
)
cases = sorted(set(random_string(string_length) for _ in range(n)))
body = "".join(
f' case "{test_case}": return {random.randint(0, 2 ** 16)}\n' for i, test_case in enumerate(cases)
)
footer = dedent(
f"""
}}
return 0
}}
"""
)
return header + body + footer
def generate_bench_func(name: str, string_length: int) -> str:
return dedent(
f"""
func Benchmark{name}(b *testing.B) {{
pattern := RandomString({string_length})
for b.Loop() {{
{name}(pattern)
}}
}}
"""
)
def generate_main_go(n_cases: list[int], string_lengths: list[int], seed: int = 42) -> str:
funcs = []
for n in n_cases:
for length in string_lengths:
random.seed(seed)
if_else_func = generate_if_else_func(name=f"MatchIfElse_{n}_{length}", n=n, string_length=length)
random.seed(seed)
switch_case_func = generate_switch_case_func(name=f"MatchSwitchCase_{n}_{length}", n=n, string_length=length)
random.seed(seed)
map_func = generate_map_func(name=f"MatchMap_{n}_{length}", n=n, string_length=length)
random.seed(seed)
map_func_prealloc = generate_preallocated_map_func(name=f"MatchMapPrealloc_{n}_{length}", n=n, string_length=length)
funcs += [if_else_func, switch_case_func, map_func, map_func_prealloc]
header = dedent(
"""
package main
"""
)
body = "\n\n".join(funcs)
footer = dedent(
"""
func main(){}
"""
)
return header + body + footer
def generate_code_file(output_dir: Path, n_cases: list[int], string_lengths: list[int], seed: int = 42) -> None:
code = generate_main_go(n_cases, string_lengths, seed)
output_dir.mkdir(exist_ok=True, parents=True)
file = (output_dir/"main.go")
file.write_text(code)
print(f"Created {file}")
def generate_main_test_go(n_cases: list[int], string_lengths: list[int], seed: int = 42):
funcs = []
for n in n_cases:
for length in string_lengths:
random.seed(seed)
if_else_func = generate_bench_func(name=f"MatchIfElse_{n}_{length}", string_length=length)
random.seed(seed)
switch_case_func = generate_bench_func(name=f"MatchSwitchCase_{n}_{length}", string_length=length)
random.seed(seed)
map_func = generate_bench_func(name=f"MatchMap_{n}_{length}", string_length=length)
random.seed(seed)
map_func_prealloc = generate_bench_func(name=f"MatchMapPrealloc_{n}_{length}", string_length=length)
funcs += [if_else_func, switch_case_func, map_func, map_func_prealloc]
header = dedent(
"""
package main
import (
"testing"
"math/rand"
"time"
)
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func RandomString(length int) string {
seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}
"""
)
body = "\n\n".join(funcs)
return header + body
def generate_test_file(output_dir: Path, n_cases: list[int], string_lengths: list[int], seed: int = 42) -> None:
code = generate_main_test_go(n_cases, string_lengths, seed)
output_dir.mkdir(exist_ok=True, parents=True)
file = (output_dir/"main_test.go")
file.write_text(code)
print(f"Created {file}")
def generate_go_mod_file(output_dir: Path) -> None:
code = dedent(
"""
module benchmarking-matching
go 1.24.1
"""
).strip()
output_dir.mkdir(exist_ok=True, parents=True)
file = (output_dir/"go.mod")
file.write_text(code)
print(f"Created {file}")
def generate_golang_files(output_dir: Path, n_cases: list[int], string_lengths: list[int], seed: int = 42) -> None:
generate_go_mod_file(output_dir)
generate_code_file(output_dir, n_cases=n_cases, string_lengths=string_lengths, seed=seed)
generate_test_file(output_dir, n_cases=n_cases, string_lengths=string_lengths, seed=seed)
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument("-d", "--dir", type=Path)
parser.add_argument("-n", "--num_cases", type=str, help="comma delimited list of number of cases in a match function")
parser.add_argument("-l", "--string_lengths", type=str, help="comma delimited list of string lengths in a match function")
args = parser.parse_args()
generate_golang_files(
output_dir=args.dir,
n_cases=[int(n) for n in args.num_cases.split(',')],
string_lengths=[int(n) for n in args.string_lengths.split(',')],
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment