Skip to content
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

Improve Performance by Prefixing List and TCO, Bump to 0.4.0 #15

Merged
merged 1 commit into from
Nov 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# 0.4.0
* Improve speed with list prefixing and tail-call optimizations.
# 0.3.1
* Upgrade elixir version. (https://github.com/exthereum/ex_rlp/pull/14)
# 0.3.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Add `:ex_rlp` as a dependency to your project's `mix.exs`:
```elixir
defp deps do
[
{:ex_rlp, "~> 0.3.1"}
{:ex_rlp, "~> 0.4.0"}
]
end
```
Expand Down
9 changes: 5 additions & 4 deletions lib/ex_rlp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule ExRLP do
@type t :: any()

@doc """
Given an RLP structure, returns the encoding as a string.
Given an RLP structure (i.e. anything), returns the encoding as a binary.

## Examples

Expand Down Expand Up @@ -52,11 +52,12 @@ defmodule ExRLP do
@spec encode(t) :: binary()
@spec encode(t, keyword()) :: binary()
def encode(item, options \\ [encoding: :binary]) do
item |> Encode.encode(options)
Encode.encode(item, options)
end

@doc """
Given an RLP-encoded string, returns a decoded RPL structure (which is an array of RLP structures or binaries).
Given an RLP-encoded string, returns a decoded RLP structure (which is an
array of RLP structures or binaries).

## Examples

Expand Down Expand Up @@ -102,6 +103,6 @@ defmodule ExRLP do
@spec decode(binary()) :: t
@spec decode(binary(), keyword()) :: t
def decode(item, options \\ [encoding: :binary]) do
item |> Decode.decode(options)
Decode.decode(item, options)
end
end
108 changes: 39 additions & 69 deletions lib/ex_rlp/decode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,113 +3,83 @@ defmodule ExRLP.Decode do

@spec decode(binary(), keyword()) :: ExRLP.t()
def decode(item, options \\ []) when is_binary(item) do
item
|> maybe_decode_hex(Keyword.get(options, :encoding, :binary))
|> decode_item
case item
|> unencode(Keyword.get(options, :encoding, :binary))
|> decode_item do
[res] -> res
[] -> nil
els -> els
end
end

@spec maybe_decode_hex(binary(), atom()) :: binary()
defp maybe_decode_hex(value, :binary), do: value
defp maybe_decode_hex(value, :hex), do: decode_hex(value)
@spec unencode(binary() | String.t(), atom()) :: binary()
defp unencode(value, :binary), do: value
defp unencode(value, :hex), do: Base.decode16!(value, case: :lower)

@spec decode_item(binary(), ExRLP.t()) :: ExRLP.t()
defp decode_item(rlp_binary, result \\ nil)
defp decode_item(rlp_binary, result \\ [])

defp decode_item("", result) do
result
# When we don't have any RLP left, return the accumulator
defp decode_item(<<>>, result) do
Enum.reverse(result)
end

# Decodes the head represents an item to be added directly
# to the result.
defp decode_item(<<(<<prefix>>), tail::binary>>, result) when prefix < 128 do
new_item = <<prefix>>

new_result = result |> add_new_item(new_item)

decode_item(tail, new_result)
decode_item(tail, [<<prefix>> | result])
end

# Decodes medium length-binary?
defp decode_item(<<(<<prefix>>), tail::binary>>, result) when prefix <= 183 do
{new_item, new_tail} = decode_medium_binary(prefix, tail, 128)

new_result = result |> add_new_item(new_item)

decode_item(new_tail, new_result)
decode_item(new_tail, [new_item | result])
end

# Decodes long length-binary?
defp decode_item(<<(<<be_size_prefix>>), tail::binary>>, result) when be_size_prefix < 192 do
{new_item, new_tail} = decode_long_binary(be_size_prefix, tail, 183)

new_result = result |> add_new_item(new_item)

decode_item(new_tail, new_result)
end

defp decode_item(<<(<<be_size_prefix>>), tail::binary>>, result) when be_size_prefix == 192 do
new_item = []

new_result = result |> add_new_item(new_item)

decode_item(tail, new_result)
decode_item(new_tail, [new_item | result])
end

defp decode_item(<<(<<prefix>>), tail::binary>>, result) when prefix <= 247 do
{list, new_tail} = decode_medium_binary(prefix, tail, 192)

new_result = result |> add_decoded_list(list)

decode_item(new_tail, new_result)
# Decodes an empty list
defp decode_item(<<(<<be_size_prefix>>), tail::binary>>, result)
when be_size_prefix == 192 do
decode_item(tail, [[] | result])
end

defp decode_item(<<(<<be_size_prefix>>), tail::binary>>, result) do
{list, new_tail} = decode_long_binary(be_size_prefix, tail, 247)

new_result = result |> add_decoded_list(list)
defp decode_item(<<(<<prefix>>), tail::binary>>, result) do
{list_bin, new_tail} =
if prefix <= 247 do
decode_medium_binary(prefix, tail, 192)
else
decode_long_binary(prefix, tail, 247)
end

decode_item(new_tail, new_result)
end
next_list = decode_item(list_bin, [])

@spec add_new_item(ExRLP.t(), ExRLP.t()) :: ExRLP.t()
def add_new_item(nil, new_item) do
new_item
end

def add_new_item(result, new_item) do
result ++ [new_item]
end

@spec add_decoded_list(ExRLP.t(), binary()) :: ExRLP.t()
defp add_decoded_list(nil, rlp_list_binary) do
decode_item(rlp_list_binary, [])
end

defp add_decoded_list(result, rlp_list_binary) do
list_items = decode_item(rlp_list_binary, [])

result ++ [list_items]
decode_item(new_tail, [next_list | result])
end

@spec decode_medium_binary(integer(), binary(), integer()) :: {binary(), binary()}
defp decode_medium_binary(length_prefix, tail, prefix) do
item_length = length_prefix - prefix
<<item::binary-size(item_length), new_tail::binary>> = tail
<<item::binary-size(item_length), new_tail::binary()>> = tail

{item, new_tail}
end

@spec decode_long_binary(integer(), binary(), integer()) :: {binary(), binary()}
defp decode_long_binary(be_size_prefix, tail, prefix) do
be_size = be_size_prefix - prefix
<<be::binary-size(be_size), data::binary>> = tail
<<be::binary-size(be_size), data::binary()>> = tail

item_length = be |> :binary.decode_unsigned()
<<item::binary-size(item_length), new_tail::binary>> = data
item_length = :binary.decode_unsigned(be)

{item, new_tail}
end
<<item::binary-size(item_length), new_tail::binary()>> = data

@spec decode_hex(binary()) :: binary()
defp decode_hex(binary) do
{:ok, decoded_binary} = binary |> Base.decode16(case: :lower)

decoded_binary
{item, new_tail}
end
end
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ defmodule ExRLP.Mixfile do
def project do
[
app: :ex_rlp,
version: "0.3.1",
version: "0.4.0",
elixir: "~> 1.7",
description: "Ethereum's Recursive Length Prefix (RLP) encoding",
package: [
maintainers: ["Ayrat Badykov", "Geoffrey Hayes"],
licenses: ["MIT"],
links: %{"GitHub" => "https://github.com/exthereum/ex_rlp"}
links: %{"GitHub" => "https://github.com/mana-ethereum/ex_rlp"}
],
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
Expand Down
25 changes: 25 additions & 0 deletions test/ex_rlp/performance_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule ExRLP.PerformanceTest do
use ExUnit.Case

@large_rlp (for _ <- 1..100_000 do
[<<1>>, [<<1>>, <<2>>]]
end)

test "large rlp list" do
start = time_start()

assert @large_rlp
|> ExRLP.encode()
|> ExRLP.decode() == @large_rlp

assert elapsed(start) < 20
end

defp time_start() do
Time.utc_now()
end

defp elapsed(start) do
Time.diff(Time.utc_now(), start, :second)
end
end