Performance comparison for JSON generation in Ruby
This is the translation of this article originally posted by Rafael Barbolo.
During the last days, I've been optimizing a Ruby on Rails application of an API that responds to requests in JSON or XML formats.
I've identified memory bloats in some rare cases of XML generation and that’s why I’m investigating efficient ways to generate JSON and XML. Currently, this API uses the RABL gem to render response documents.
My first mission is to find a rendering solution with the best performance. I’m willing to sacrifice simplicity, aesthetics, standardization and code maintenance.
The API that is being optimized gets tens of millions of requests per month, so performance is key here.
TL;DR
The best solution I've found is using the Oj library to manipulate JSONs and not using any DSL renderer. In other words, I build a Hash
from a Ruby object that gets converted directly to JSON.
Performance to convert a Hash to JSON
The first performance analysis will compare some supposedly efficient libraries that allow you to convert a Ruby Hash
to a JSON:
In order to make this analysis easier, the gem MultiJSON will be used. It offers a common interface to the JSON transformation libraries.
The conversion of 3 types of Hash
objects will be evaluated:
- hash1- very simple with few keys and basic types.
- hash2 - reasonable number of keys, “trees” with up to 3 nesting levels, values of
Array
ofHashes
type, value with longString
(simulating the contents of a file, for example). I suppose this is the most commonHash
in typical Rails applications. - hash3 - a thousand keys, “tree” with up to 10 nesting levels, values with type hash1 and hash2. This is an example of an extreme case.
Results
To evaluate the results, a script was used in order to measure how long it takes to convert a Hash
to a JSON and also how much memory is allocated during the conversion process.
╔════════════╦═════════╦═════════╗
║ BIBLIOTECA ║ TEMPO ║ MEMÓRIA ║
╔════════╬════════════╬═════════╬═════════╣
║ hash1 ║ ║ ║ ║
║ ║ JSON ║ 0,01 ms ║ 1,5 K ║
║ ║ Yajl ║ 0,02 ms ║ 1,0 K ║
║ ║ Oj ║ 0,01 ms ║ 1,0 K ║
╠════════╬════════════╬═════════╬═════════╣
║ hash2 ║ ║ ║ ║
║ ║ JSON ║ 0,57 ms ║ 12,2 K ║
║ ║ Yajl ║ 0,39 ms ║ 11,8 K ║
║ ║ Oj ║ 0,33 ms ║ 11,8 K ║
╠════════╬════════════╬═════════╬═════════╣
║ hash3 ║ ║ ║ ║
║ ║ JSON ║ 327 ms ║ 6,62 M ║
║ ║ Yajl ║ 235 ms ║ 6,62 M ║
║ ║ Oj ║ 235 ms ║ 6,62 M ║
╚════════╩════════════╩═════════╩═════════╝
In all scenarios, the Oj library had the best performance in terms of time to convert a Hash
to a JSON. No library presented significantly differences in memory allocation.
Performance for JSON rendering
Rendering a JSON typically involves interpreting conversion rules (in a template) of an input Ruby object to an output JSON.
The most popular JSON renderer in Ruby is JBuilder. In the past, a popular one was RABL. And a less conventional approach that has been evaluated consists of building a Hash
from a Ruby object followed by the direct conversion of that Hash
to JSON.
In this analysis, the Ruby object to be rendered represents an Article (post) with multiple Comments. 3 types of articles were evaluated:
- post1- article with approximately a thousand characters and 10 simple comments.
- post2 - article with approximately 1 million characters and 200 comments, each with more than a thousand characters.
- post3 - article with approximately 10 million characters and 5 thousand comments, each with more than 10 thousands characters.
Results
To evaluate the results, a script was used in order to measure how long it takes to render each article to JSON and also how much memory is allocated during the rendering process.
╔═════════════╦═════════╦═════════╗
║ Render ║ TEMPO ║ MEMÓRIA ║
╔════════╬═════════════╬═════════╬═════════╣
║ post1 ║ ║ ║ ║
║ ║ RABL ║ 0,96 ms ║ 82,7 K ║
║ ║ Jbuilder ║ 0,22 ms ║ 30,1 K ║
║ ║ Hash > JSON ║ 0,09 ms ║ 10,5 K ║
╠════════╬═════════════╬═════════╬═════════╣
║ post2 ║ ║ ║ ║
║ ║ RABL ║ 14,3 ms ║ 1,20 M ║
║ ║ Jbuilder ║ 6,8 ms ║ 0,53 M ║
║ ║ Hash > JSON ║ 4,6 ms ║ 0,19 M ║
╠════════╬═════════════╬═════════╬═════════╣
║ post3 ║ ║ ║ ║
║ ║ RABL ║ 475 ms ║ 29,4 M ║
║ ║ Jbuilder ║ 321 ms ║ 13,2 M ║
║ ║ Hash > JSON ║ 259 ms ║ 4,7 M ║
╚════════╩═════════════╩═════════╩═════════╝
In all scenarios, the Hash > JSON approach (conversion ofthe Ruby object to a Hash
and then conversion of the Hash
to a JSON) performed best in terms of execution time and memory allocation.
What really draws my attention is that the Hash > JSON approach uses about 6 times less memory than RABL and 3 times less memory than Jbuilder.
Final code
I’m still thinking about the organization of the final code. For now, my suggestion is:
Controller
# app/controllers/posts_controller.rbrespond_to do |format|
format.json { render json: Renderer::Post.json(@post) }
format.xml { ... }
end
Renderer
# app/views-api/renderer/post.rbclass Renderer::Post
def self.json(post)
MultiJson.dump({
'content' => post.content,
'created_at' => post.created_at,
'published' => post.published,
'author' => {
'name' => post.author.name,
'age' => post.author.age,
'email' => post.author.email,
},
'comments' => post.comments.map do |comment|
{
'created_at' => comment.created_at,
'message' => comment.message,
'user' => {
'name' => comment.user.name,
'age' => comment.user.age,
'email' => comment.user.email,
},
'attachment' => comment.attachment,
}
end
})
end
end
Environment used in this analysis
The execution time of each test case is an average of the execution times of one thousand runs of each scenario.
The source code needed to reproduce the scenarios can be found in this gist.
The environment used in the tests is described below:
- Computer: MacBook Pro 15" 2017