我想知道为什么包含大字符串的结构的json序列化在晶体中很慢。
以下代码的性能差:
struct Page
include AutoJson
field :uri, String
field :html, String
end
page = Page.new(url, html) # html is a string containing ±128KB of html
page.to_json
javaScript(node.js)或go中的以下代码几乎是瞬时的(例如x10〜x20倍):
node.js
page = { url: url, html: html }
JSON.stringify(page)
GO
type Page struct {
Uri string `json="uri"`
Html string `json="html"`
}
page = Page{ uri, html }
json, _ = json.Marshal(page)
考虑水晶通常很快(与GO相当,比V8 JavaScript快得多),这让我想知道这里发生了什么。
我一直在尝试一些晶体代码,似乎这里的罪名是大字符串的双引号字符串逃脱(显然是在序列化JSON对象时需要的)。但是,为什么我不知道要花这么长时间(多个分配,副本?)。
对于记录,在这些示例中,html
是使用可用的任何同步方法从磁盘加载的大约128KB HTML文件。在对这些片段进行基准测试时,显然没有考虑文件阅读操作。
i用Crystal 0.25.1(LLVM 6.0.1),GO 1.10.3,Node.js v8.11.2在MacOS X86_64上进行了测试。
示例全部读取一个161 kb的HTML文件,打开一个tempfile并执行序列化页面对象并将其写入文件的10.000迭代。
这会产生约1.5 GB的JSON,该系统具有非常快的PCIE SSD,因此IO吞吐量不是瓶颈。
我选择实际将数据写入文件以确保编译器无法优化函数调用。
Crystal
require "json"
require "tempfile"
url = "http://www.example.org"
html = File.read("index.html")
record(Page, uri : String, html : String) do
include JSON::Serializable
end
Tempfile.open("foo") do |io|
10_000.times do
page = Page.new(url, html)
page.to_json(io)
end
end
GO
package main
import (
"encoding/json"
"io/ioutil"
"log"
"os"
)
type Page struct {
Uri string `json="uri"`
Html string `json="html"`
}
func main() {
buf, err := ioutil.ReadFile("index.html")
if err != nil {
log.Fatal(err)
}
uri := "http://www.example.org"
html := string(buf)
file, err := ioutil.TempFile(os.TempDir(), "foo")
if err != nil {
log.Fatal(err)
}
defer os.Remove(file.Name())
for i := 0; i < 10000; i++ {
page := Page{uri, html}
json, err := json.Marshal(page)
if err != nil {
log.Fatal(err)
}
_, err = file.Write(json)
if err != nil {
log.Fatal(err)
}
}
}
node.js
const fs = require('fs')
const tmp = require('tmp')
const uri = 'http://www.example.org'
const html = fs.readFileSync('index.html')
tmp.file((err, path, fd) => {
if (err) throw err;
for(let i = 0; i < 10000; i++) {
const page = { uri, html }
const json = JSON.stringify(page)
fs.writeSync(fd, json)
}
})
结果
- GO:10.88秒,8.5 MB RAM
- 水晶:12.62秒,2 MB RAM,1.16X作为慢
- node.js:101.82秒,75 MB RAM,9.36X作为慢
请注意,我用--release
编译了Crystal示例,并更新了0.25.1的代码。
node.js示例使用了V8,而不是V10,因为V10与我用于tempfiles的node-tmp
NPM模块不相容。
基准测试是在2015年初的13英寸MacBook Pro上完成的,带有i7-5557U CPU,16 GB RAM和1 TB PCIE SSD。
与许多其他API一样,Crystal的JSON实现并未真正优化速度。它只是为了使它起作用。实际上,对于大多数用例中,这实际上已经很快了,但是当然有很大的改进正在等待。
我不确定这里的原因是什么。它可能与字符串逃脱有关,但这也需要用其他语言来完成。
关于与JavaScript的比较,将对象转换为JSON实际上是相当性能的,因为这是JavaScript的本机数据类型,并且非常有效地实现。这不是动态代码评估,而是在JavaScript VM中编译的。
尝试:
crystal build test.cr --release --no-debug
如果这无法解决问题,那么值得在https://github.com/crystal-lang/crystal/issues/issues
上创建票re:---no-debug
--no-debug
标志可能不是必需的,但是在撰写本文时,有一个空旷的问题,表明在某些情况下是:
https://github.com/crystal-lang/crystal/issues/4880