Skip to content

Commit

Permalink
refactor: unroll decoding
Browse files Browse the repository at this point in the history
From 1.5x to 1.8 speedupt
  • Loading branch information
ahamez committed Jan 27, 2025
1 parent d90b929 commit 65188b9
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 5 deletions.
47 changes: 46 additions & 1 deletion lib/varint/leb128.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,52 @@ defmodule Varint.LEB128 do
** (ArgumentError) not a valid LEB128 encoded integer
"""
@spec decode(binary) :: {non_neg_integer, binary}
def decode(b) when is_binary(b), do: do_decode(0, 0, b)
def decode(<<0::1, byte0::7, rest::binary>>),
do: {byte0, rest}

def decode(<<1::1, byte1::7, 0::1, byte0::7, rest::binary>>),
do: {byte1 <<< 0 ||| byte0 <<< 7, rest}

def decode(<<1::1, byte2::7, 1::1, byte1::7, 0::1, byte0::7, rest::binary>>),
do: {byte2 <<< 0 ||| byte1 <<< 7 ||| byte0 <<< 14, rest}

def decode(<<1::1, byte3::7, 1::1, byte2::7, 1::1, byte1::7, 0::1, byte0::7, rest::binary>>),
do: {byte3 <<< 0 ||| byte2 <<< 7 ||| byte1 <<< 14 ||| byte0 <<< 21, rest}

def decode(
<<1::1, byte4::7, 1::1, byte3::7, 1::1, byte2::7, 1::1, byte1::7, 0::1, byte0::7,
rest::binary>>
),
do: {byte4 <<< 0 ||| byte3 <<< 7 ||| byte2 <<< 14 ||| byte1 <<< 21 ||| byte0 <<< 28, rest}

def decode(
<<1::1, byte5::7, 1::1, byte4::7, 1::1, byte3::7, 1::1, byte2::7, 1::1, byte1::7, 0::1,
byte0::7, rest::binary>>
),
do:
{byte5 <<< 0 ||| byte4 <<< 7 ||| byte3 <<< 14 ||| byte2 <<< 21 ||| byte1 <<< 28 |||
byte0 <<< 35, rest}

def decode(
<<1::1, byte6::7, 1::1, byte5::7, 1::1, byte4::7, 1::1, byte3::7, 1::1, byte2::7, 1::1,
byte1::7, 0::1, byte0::7, rest::binary>>
),
do:
{byte6 <<< 0 ||| byte5 <<< 7 ||| byte4 <<< 14 ||| byte3 <<< 21 ||| byte2 <<< 28 |||
byte1 <<< 35 |||
byte0 <<< 42, rest}

def decode(
<<1::1, byte7::7, 1::1, byte6::7, 1::1, byte5::7, 1::1, byte4::7, 1::1, byte3::7, 1::1,
byte2::7, 1::1, byte1::7, 0::1, byte0::7, rest::binary>>
),
do:
{byte7 <<< 0 ||| byte6 <<< 7 ||| byte5 <<< 14 ||| byte4 <<< 21 ||| byte3 <<< 28 |||
byte2 <<< 35 |||
byte1 <<< 42 |||
byte0 <<< 49, rest}

def decode(b), do: do_decode(0, 0, b)

# -- Private

Expand Down
37 changes: 33 additions & 4 deletions test/varint/leb128_test.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
defmodule VarintSimple do
import Bitwise

@spec encode(integer) :: binary()
def encode(v) when v < 1 <<< 7, do: <<v>>
def encode(v), do: <<1::1, v::7, encode(v >>> 7)::binary>>

@spec decode(binary) :: {non_neg_integer, binary}
def decode(<<0::1, byte0::7, rest::binary>>), do: {byte0, rest}
def decode(b), do: do_decode(0, 0, b)

@spec do_decode(non_neg_integer, non_neg_integer, binary) :: {non_neg_integer, binary}
defp do_decode(result, shift, <<0::1, byte::7, rest::binary>>) do
{result ||| byte <<< shift, rest}
end

defp do_decode(result, shift, <<1::1, byte::7, rest::binary>>) do
do_decode(
result ||| byte <<< shift,
shift + 7,
rest
)
end
end

defmodule Varint.LEB128Test do
import Bitwise
use ExUnit.Case, async: true
Expand All @@ -7,7 +32,14 @@ defmodule Varint.LEB128Test do

property "Unrolled encoding produces the same result as the reference implementation" do
check all int <- integer(0..(1 <<< 64)) do
assert Varint.LEB128.encode(int) == encode_reference(int)
assert Varint.LEB128.encode(int) == VarintSimple.encode(int)
end
end

property "Unrolled decoding produces the same result as the reference implementation" do
check all int <- integer(0..(1 <<< 64)) do
encoded = VarintSimple.encode(int)
assert Varint.LEB128.decode(encoded) == VarintSimple.decode(encoded)
end
end

Expand Down Expand Up @@ -52,7 +84,4 @@ defmodule Varint.LEB128Test do
test "Decode raises an error for non-LEB128 encoded data" do
assert_raise ArgumentError, fn -> Varint.LEB128.decode(<<255>>) end
end

defp encode_reference(v) when v < 1 <<< 7, do: <<v>>
defp encode_reference(v), do: <<1::1, v::7, encode_reference(v >>> 7)::binary>>
end

0 comments on commit 65188b9

Please sign in to comment.