Skip to content

Commit

Permalink
🐎 Make Timex.Duration.parse ~2x faster
Browse files Browse the repository at this point in the history
It's very important for us at Duffel speedup the duration
parsing. We're were investigating this function and we
noticed a big number of calls to String.contains? that is
a O(n) order algorithm in a loop.

So we did a quick experiment on putting a flag on the number
type while reading the chars. So it assumes that is a integer,
until find a dot, where it changes the type to float. Then, it
parses using Float or Integer depending on the flag. Avoiding
the String.contains calls.

The results were quite interesting:

Before
Timex.Duration.parse		500000	3322687 ~6.65µs/op
Timex.Duration.parse		500000	1710993 ~3.42µs/op
  • Loading branch information
ulissesalmeida authored and bitwalker committed Jun 20, 2022
1 parent ed146ee commit 767de3f
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 29 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added/Changed

- Changed `Timex.Duration.Parse` to be 2x faster

### Fixed

---
Expand Down
5 changes: 5 additions & 0 deletions bench/dateformat_bench.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule Timex.Timex.Bench do

@datetime "2014-07-22T12:30:05Z"
@datetime_zoned "2014-07-22T12:30:05+02:00"
@duration "P15Y3M2DT1H14M37.25S"

setup_all do
Application.ensure_all_started(:tzdata)
Expand Down Expand Up @@ -52,4 +53,8 @@ defmodule Timex.Timex.Bench do
_ = Timex.local
:ok
end

bench "Timex.Duration.parse" do
{:ok, _} = Timex.Duration.parse(@duration)
end
end
53 changes: 24 additions & 29 deletions lib/parse/duration/parsers/iso8601.ex
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ defmodule Timex.Parse.Duration.Parsers.ISO8601Parser do
do: {:error, "unexpected end of input at #{<<c::utf8>>}"}

defp parse_components(<<c::utf8, rest::binary>>, acc) when c in @numeric do
case parse_component(rest, <<c::utf8>>) do
case parse_component(rest, {:integer, <<c::utf8>>}) do
{:error, _} = err -> err
{u, n, rest} -> parse_components(rest, [{u, n} | acc])
end
Expand All @@ -122,48 +122,43 @@ defmodule Timex.Parse.Duration.Parsers.ISO8601Parser do
defp parse_components(<<c::utf8, _::binary>>, _acc),
do: {:error, "expected numeric, but got #{<<c::utf8>>}"}

@spec parse_component(binary, binary) :: {integer, number, binary}
@spec parse_component(binary, {:float | :integer, binary}) ::
{integer, number, binary} | {:error, msg :: binary()}
defp parse_component(<<c::utf8>>, _acc) when c in @numeric,
do: {:error, "unexpected end of input at #{<<c::utf8>>}"}

defp parse_component(<<c::utf8>>, acc) when c in 'WYMDHS' do
cond do
String.contains?(acc, ".") ->
case Float.parse(acc) do
{n, _} -> {c, n, <<>>}
:error -> {:error, "invalid number `#{acc}`"}
end

:else ->
case Integer.parse(acc) do
{n, _} -> {c, n, <<>>}
:error -> {:error, "invalid number `#{acc}`"}
end
defp parse_component(<<c::utf8>>, {type, acc}) when c in 'WYMDHS' do
case cast_number(type, acc) do
{n, _} -> {c, n, <<>>}
:error -> {:error, "invalid number `#{acc}`"}
end
end

defp parse_component(<<c::utf8, rest::binary>>, acc) when c in @numeric do
parse_component(rest, <<acc::binary, c::utf8>>)
defp parse_component(<<".", rest::binary>>, {:integer, acc}) do
parse_component(rest, {:float, <<acc::binary, ".">>})
end

defp parse_component(<<c::utf8, rest::binary>>, acc) when c in 'WYMDHS' do
cond do
String.contains?(acc, ".") ->
case Float.parse(acc) do
{n, _} -> {c, n, rest}
:error -> {:error, "invalid number `#{acc}`"}
end
defp parse_component(<<c::utf8, rest::binary>>, {:integer, acc}) when c in @numeric do
parse_component(rest, {:integer, <<acc::binary, c::utf8>>})
end

:else ->
case Integer.parse(acc) do
{n, _} -> {c, n, rest}
:error -> {:error, "invalid number `#{acc}`"}
end
defp parse_component(<<c::utf8, rest::binary>>, {:float, acc}) when c in @numeric do
parse_component(rest, {:float, <<acc::binary, c::utf8>>})
end

defp parse_component(<<c::utf8, rest::binary>>, {type, acc}) when c in 'WYMDHS' do
case cast_number(type, acc) do
{n, _} -> {c, n, rest}
:error -> {:error, "invalid number `#{acc}`"}
end
end

defp parse_component(<<c::utf8>>, _acc), do: {:error, "unexpected token #{<<c::utf8>>}"}

defp parse_component(<<c::utf8, _::binary>>, _acc),
do: {:error, "unexpected token #{<<c::utf8>>}"}

@spec cast_number(:float | :integer, binary) :: {number(), binary()} | :error
defp cast_number(:integer, binary), do: Integer.parse(binary)
defp cast_number(:float, binary), do: Float.parse(binary)
end

0 comments on commit 767de3f

Please sign in to comment.