vt-elixir / ja_serializer

JSONAPI.org Serialization in Elixir.
Other
640 stars 148 forks source link

Perf improvement - Inline DSL attributes #245

Closed DocX closed 7 years ago

DocX commented 7 years ago

In our application we use JaSerializer.PhoenixView and we noticed rather poor performance when rendering lot of data with big models.

I did some mix profiler.fprof tracing and noticed there is lot of function calls caused by that each attribute in the serializer module genrated by DSL is a method call, which is even called twice to translate from /2 to /1 signature.

Even tho in the end it is all simple Map.get operations, the pure fact of invoking method calls have some overhead in Elixir's (resp. Erlang's) VM.

Here is a patch that using Elixir's macros generate attributes map compile time. On top of that I modified attributes macro to avoid apply and hence to make use of @compile {:inline} to inline each of the attribute methods during compile time as well. The result is that call to attributes doesn't have descendant method calls and instead the whole map construction is inlined.

I also added benchmark for those, here are results (MacBook Pro 2.7 GHz Intel Core i5):

master (b5d9f1d):

$ mix bench bench/ja_serializer/phoenix_view_bench.exs
Settings:
  duration:      1.0 s

## JaSerializer.PhoenixViewBench
[18:55:24] 1/6: attributes map big data
[18:55:26] 2/6: attributes map small data
[18:55:33] 3/6: render index 25 items map big data
[18:55:36] 4/6: render index 25 items small data
[18:55:38] 5/6: render index one item map big data
[18:55:41] 6/6: render index one item small data

Finished in 19.17 seconds

## JaSerializer.PhoenixViewBench
attributes map small data             10000000   0.68 µs/op
attributes map big data                 500000   3.14 µs/op # <-- Notice difference on big data
render index one item small data        100000   18.64 µs/op
render index one item map big data       50000   41.41 µs/op
render index 25 items small data          5000   329.95 µs/op
render index 25 items map big data        2000   961.51 µs/op

inline-dsl-attributes:

$ mix bench bench/ja_serializer/phoenix_view_bench.exs
Settings:
  duration:      1.0 s

## JaSerializer.PhoenixViewBench
[18:57:02] 1/6: attributes map big data
[18:57:13] 2/6: attributes map small data
[18:57:16] 3/6: render index 25 items map big data
[18:57:18] 4/6: render index 25 items small data
[18:57:21] 5/6: render index one item map big data
[18:57:24] 6/6: render index one item small data

Finished in 23.11 seconds

## JaSerializer.PhoenixViewBench
attributes map small data             10000000   0.23 µs/op
attributes map big data               10000000   0.97 µs/op # <-- Notice difference on big data
render index one item small data        100000   17.17 µs/op
render index one item map big data       50000   38.33 µs/op
render index 25 items small data         10000   257.75 µs/op
render index 25 items map big data        2000   794.38 µs/op

As you can see there is definitive boost in the attributes function. The overall render is not much better, but it is still better.

There is definitely more room to go - inline other serialized fields like meta, id, ... - And perhaps then inline the whole data map for given object. Not sure how far this can be stretched before it is "needed" to go runtime.

Let me know what you think.

alanpeabody commented 7 years ago

@DocX thanks a bunch! I have tried doing something like this a couple of times and fallen short, so much so that I had been just suggesting not using the attributes macro unless you really need it.

I am going to take a bit of time and study this, but I will likely merge this weekend.