diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f071d0..26b1f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 96c0af9..161260e 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/lib/ex_rlp.ex b/lib/ex_rlp.ex index 5e78a97..9e4d4d5 100644 --- a/lib/ex_rlp.ex +++ b/lib/ex_rlp.ex @@ -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 @@ -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 @@ -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 diff --git a/lib/ex_rlp/decode.ex b/lib/ex_rlp/decode.ex index dc225b0..db70d87 100644 --- a/lib/ex_rlp/decode.ex +++ b/lib/ex_rlp/decode.ex @@ -3,94 +3,70 @@ 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(<<(<>), tail::binary>>, result) when prefix < 128 do - new_item = <> - - new_result = result |> add_new_item(new_item) - - decode_item(tail, new_result) + decode_item(tail, [<> | result]) end + # Decodes medium length-binary? defp decode_item(<<(<>), 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(<<(<>), 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(<<(<>), 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(<<(<>), 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(<<(<>), tail::binary>>, result) + when be_size_prefix == 192 do + decode_item(tail, [[] | result]) end - defp decode_item(<<(<>), 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(<<(<>), 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 - <> = tail + <> = tail {item, new_tail} end @@ -98,18 +74,12 @@ defmodule ExRLP.Decode do @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 - <> = tail + <> = tail - item_length = be |> :binary.decode_unsigned() - <> = data + item_length = :binary.decode_unsigned(be) - {item, new_tail} - end + <> = 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 diff --git a/mix.exs b/mix.exs index f0f63e2..7524afc 100644 --- a/mix.exs +++ b/mix.exs @@ -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, diff --git a/test/ex_rlp/performance_test.exs b/test/ex_rlp/performance_test.exs new file mode 100644 index 0000000..06ef78a --- /dev/null +++ b/test/ex_rlp/performance_test.exs @@ -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