Skip to content

Commit 803a4e2

Browse files
committed
Benchmark faster Base.valid64?
1 parent af34130 commit 803a4e2

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

bench/base64_valid.exs

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
defmodule BaseAlt do
2+
@type decode_case :: :upper | :lower | :mixed
3+
4+
b64_alphabet = ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
5+
b64url_alphabet = ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
6+
7+
to_decode_list = fn alphabet ->
8+
alphabet = Enum.sort(alphabet)
9+
map = Map.new(alphabet)
10+
{min, _} = List.first(alphabet)
11+
{max, _} = List.last(alphabet)
12+
{min, Enum.map(min..max, &map[&1])}
13+
end
14+
15+
defp remove_ignored(string, nil), do: string
16+
17+
defp remove_ignored(string, :whitespace) do
18+
for <<char::8 <- string>>, char not in ~c"\s\t\r\n", into: <<>>, do: <<char::8>>
19+
end
20+
21+
@spec valid64?(binary, ignore: :whitespace, padding: boolean) :: boolean
22+
def valid64?(string, opts \\ []) when is_binary(string) do
23+
pad? = Keyword.get(opts, :padding, true)
24+
string |> remove_ignored(opts[:ignore]) |> validate64base?(pad?)
25+
end
26+
27+
@spec url_valid64?(binary, ignore: :whitespace, padding: boolean) :: boolean
28+
def url_valid64?(string, opts \\ []) when is_binary(string) do
29+
pad? = Keyword.get(opts, :padding, true)
30+
string |> remove_ignored(opts[:ignore]) |> validate64url?(pad?)
31+
end
32+
33+
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
34+
validate_name = :"validate64#{base}?"
35+
validate_main_name = :"validate_main64#{validate_name}?"
36+
valid_char_name = :"valid_char64#{base}?"
37+
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
38+
39+
defp unquote(validate_main_name)(<<>>), do: true
40+
41+
defp unquote(validate_main_name)(
42+
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
43+
) do
44+
unquote(valid_char_name)(c1) and
45+
unquote(valid_char_name)(c2) and
46+
unquote(valid_char_name)(c3) and
47+
unquote(valid_char_name)(c4) and
48+
unquote(valid_char_name)(c5) and
49+
unquote(valid_char_name)(c6) and
50+
unquote(valid_char_name)(c7) and
51+
unquote(valid_char_name)(c8) and
52+
unquote(validate_main_name)(rest)
53+
end
54+
55+
defp unquote(validate_name)(<<>>, _pad?), do: true
56+
57+
defp unquote(validate_name)(string, pad?) do
58+
segs = div(byte_size(string) + 7, 8) - 1
59+
<<main::size(^segs)-binary-unit(64), rest::binary>> = string
60+
main_valid? = unquote(validate_main_name)(main)
61+
62+
case rest do
63+
_ when not main_valid? ->
64+
false
65+
66+
<<c1::8, c2::8, ?=, ?=>> ->
67+
unquote(valid_char_name)(c1) and
68+
unquote(valid_char_name)(c2)
69+
70+
<<c1::8, c2::8, c3::8, ?=>> ->
71+
unquote(valid_char_name)(c1) and
72+
unquote(valid_char_name)(c2) and
73+
unquote(valid_char_name)(c3)
74+
75+
<<c1::8, c2::8, c3::8, c4::8>> ->
76+
unquote(valid_char_name)(c1) and
77+
unquote(valid_char_name)(c2) and
78+
unquote(valid_char_name)(c3) and
79+
unquote(valid_char_name)(c4)
80+
81+
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, ?=, ?=>> ->
82+
unquote(valid_char_name)(c1) and
83+
unquote(valid_char_name)(c2) and
84+
unquote(valid_char_name)(c3) and
85+
unquote(valid_char_name)(c4) and
86+
unquote(valid_char_name)(c5) and
87+
unquote(valid_char_name)(c6)
88+
89+
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
90+
unquote(valid_char_name)(c1) and
91+
unquote(valid_char_name)(c2) and
92+
unquote(valid_char_name)(c3) and
93+
unquote(valid_char_name)(c4) and
94+
unquote(valid_char_name)(c5) and
95+
unquote(valid_char_name)(c6) and
96+
unquote(valid_char_name)(c7)
97+
98+
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
99+
unquote(valid_char_name)(c1) and
100+
unquote(valid_char_name)(c2) and
101+
unquote(valid_char_name)(c3) and
102+
unquote(valid_char_name)(c4) and
103+
unquote(valid_char_name)(c5) and
104+
unquote(valid_char_name)(c6) and
105+
unquote(valid_char_name)(c7) and
106+
unquote(valid_char_name)(c8)
107+
108+
<<c1::8, c2::8>> when not pad? ->
109+
unquote(valid_char_name)(c1) and
110+
unquote(valid_char_name)(c2)
111+
112+
<<c1::8, c2::8, c3::8>> when not pad? ->
113+
unquote(valid_char_name)(c1) and
114+
unquote(valid_char_name)(c2) and
115+
unquote(valid_char_name)(c3)
116+
117+
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> when not pad? ->
118+
unquote(valid_char_name)(c1) and
119+
unquote(valid_char_name)(c2) and
120+
unquote(valid_char_name)(c3) and
121+
unquote(valid_char_name)(c4) and
122+
unquote(valid_char_name)(c5) and
123+
unquote(valid_char_name)(c6)
124+
125+
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
126+
unquote(valid_char_name)(c1) and
127+
unquote(valid_char_name)(c2) and
128+
unquote(valid_char_name)(c3) and
129+
unquote(valid_char_name)(c4) and
130+
unquote(valid_char_name)(c5) and
131+
unquote(valid_char_name)(c6) and
132+
unquote(valid_char_name)(c7)
133+
134+
_ ->
135+
false
136+
end
137+
end
138+
139+
@compile {:inline, [{valid_char_name, 1}]}
140+
defp unquote(valid_char_name)(char)
141+
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
142+
do: true
143+
144+
defp unquote(valid_char_name)(_char), do: false
145+
end
146+
end
147+
148+
inputs = [
149+
"small string": Base.encode64("hello world"),
150+
"big string": Base.encode64(:crypto.strong_rand_bytes(1_000_000)),
151+
"invalid string": String.duplicate("1234567890ABCDEF", 10) |> String.replace_trailing("F", "@")
152+
]
153+
154+
Benchee.run(
155+
%{
156+
"main" => &Base.valid64?/1,
157+
"PR" => &BaseAlt.valid64?/1
158+
},
159+
inputs: inputs,
160+
memory_time: 1
161+
)

bench/base64_valid.results.txt

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
Operating System: macOS
2+
CPU Information: Apple M1
3+
Number of Available Cores: 8
4+
Available memory: 16 GB
5+
Elixir 1.19.0-dev
6+
Erlang 27.3
7+
8+
Benchmark suite executing with the following configuration:
9+
warmup: 2 s
10+
time: 5 s
11+
memory time: 1 s
12+
reduction time: 0 ns
13+
parallel: 1
14+
inputs: small string, big string, invalid string
15+
Estimated total run time: 48 s
16+
17+
Benchmarking PR with input small string ...
18+
Benchmarking PR with input big string ...
19+
Benchmarking PR with input invalid string ...
20+
Benchmarking main with input small string ...
21+
Benchmarking main with input big string ...
22+
Benchmarking main with input invalid string ...
23+
24+
##### With input small string #####
25+
Name ips average deviation median 99th %
26+
PR 4.12 M 242.53 ns ±20144.77% 83 ns 208 ns
27+
main 2.45 M 408.99 ns ±19199.13% 83 ns 208 ns
28+
29+
Comparison:
30+
PR 4.12 M
31+
main 2.45 M - 1.69x slower +166.46 ns
32+
33+
Memory usage statistics:
34+
35+
Name Memory usage
36+
PR 104 B
37+
main 104 B - 1.00x memory usage +0 B
38+
39+
**All measurements for memory usage were the same**
40+
41+
##### With input big string #####
42+
Name ips average deviation median 99th %
43+
PR 892.92 1.12 ms ±3.98% 1.11 ms 1.19 ms
44+
main 391.44 2.55 ms ±0.86% 2.55 ms 2.64 ms
45+
46+
Comparison:
47+
PR 892.92
48+
main 391.44 - 2.28x slower +1.43 ms
49+
50+
Memory usage statistics:
51+
52+
Name Memory usage
53+
PR 120 B
54+
main 120 B - 1.00x memory usage +0 B
55+
56+
**All measurements for memory usage were the same**
57+
58+
##### With input invalid string #####
59+
Name ips average deviation median 99th %
60+
PR 2.65 M 0.38 μs ±12884.12% 0.21 μs 0.33 μs
61+
main 0.90 M 1.11 μs ±2373.95% 0.79 μs 1.75 μs
62+
63+
Comparison:
64+
PR 2.65 M
65+
main 0.90 M - 2.95x slower +0.74 μs
66+
67+
Memory usage statistics:
68+
69+
Name Memory usage
70+
PR 0.117 KB
71+
main 1.04 KB - 8.87x memory usage +0.92 KB
72+
73+
**All measurements for memory usage were the same**

0 commit comments

Comments
 (0)