unicode 归类 NIF 运行 比纯 Erlang 实现慢
unicode collation NIF running slower than Pure Erlang implementation
我试图通过将其重写为 NIF 实现来优化现有的 unicode 归类库(用 Erlang 编写)。主要原因是因为整理是 CPU 密集型操作。
Link 执行:https://github.com/abhi-bit/merger
通过基于纯 Erlang 的优先级队列对 1M 行进行 Unicode 排序:
erlc *.erl; ERL_LIBS="..:$ERL_LIBS" erl -noshell -s perf_couch_skew main 1000000 -s init stop
Queue size: 1000000
12321.649 ms
通过基于 NIF 的二项式堆对 1M 行进行 Unicode 整理:
erlc *.erl; ERL_LIBS="..:$ERL_LIBS" erl -noshell -s perf_merger main 1000000 -s init stop
Queue size: 1000000
15871.965 ms
这很不寻常,我原以为它可能会快 10 倍左右。
我开启了eprof
/fprof
,但是在NIF模块方面用处不大,以下是eprof
所说的突出功能
FUNCTION CALLS % TIME [uS / CALLS]
-------- ----- --- ---- [----------]
merger:new/0 1 0.00 0 [ 0.00]
merger:new/2 1 0.00 0 [ 0.00]
merger:size/1 100002 0.31 19928 [ 0.20]
merger:in/3 100000 3.29 210620 [ 2.11]
erlang:put/2 2000000 6.63 424292 [ 0.21]
merger:out/1 100000 14.35 918834 [ 9.19]
我敢肯定,NIF 实现可以更快,因为我有一个纯 C 实现的 unicode 排序规则,它基于使用动态数组的二进制堆,而且速度要快得多。
$ make
gcc -I/usr/local/Cellar/icu4c/55.1/include -L/usr/local/Cellar/icu4c/55.1/lib min_heap.c collate_json.c kway_merge.c kway_merge_test.c -o output -licui18n -licuuc -licudata
./output
Merging 1 arrays each of size 1000000
mergeKArrays took 84.626ms
具体问题我在这里:
- 由于 NIF 模块中的 Erlang <-> C 通信,预计会减慢多少?在这种情况下,纯 C 和 NIF 实现之间的减速可能是 30 倍或更多
- 哪些工具可用于调试 NIF 相关的减速(如本例)?我尝试使用
perf top
来查看函数调用,最上面的(显示了一些十六进制地址)来自 "beam.smp".
- 我应该考虑优化 NIF 的哪些方面?例如:我听说应该尽量减少 Erlang 与 C 之间的数据传输,反之亦然,是否还有更多此类方面需要考虑?
调用 NIF 的开销很小。当 Erlang 运行时加载加载 NIF 的模块时,它会使用模拟器指令修补模块的 beam 代码以调用 NIF。在调用实现 NIF 的 C 函数之前,指令本身仅执行少量设置。这不是导致性能问题的区域。
分析 NIF 与分析任何其他 C/C++ 代码非常相似。从你的 Makefile 来看,你似乎是在 OS X 上开发这段代码。在那个平台上,假设你安装了 XCode,你可以使用 Instruments application with the CPU Samples instrument to see where your code is spending most of its time. On Linux, you can use the callgrind tool of valgrind together with an Erlang emulator built with valgrind support 来衡量你的代码。
如果您在代码中使用这些工具,您会发现,例如,perf_merger:main/1
将大部分时间花在 merger_nif_heap_get
上,而 merger_nif_heap_get
又花费了大量时间在 CollateJSON
。该函数似乎调用了 convertUTF8toUChar
和 createStringFromJSON
很多次。您的 NIF 似乎也执行大量内存分配。这些是您应该关注的领域,以加速您的代码。
我试图通过将其重写为 NIF 实现来优化现有的 unicode 归类库(用 Erlang 编写)。主要原因是因为整理是 CPU 密集型操作。
Link 执行:https://github.com/abhi-bit/merger
通过基于纯 Erlang 的优先级队列对 1M 行进行 Unicode 排序:
erlc *.erl; ERL_LIBS="..:$ERL_LIBS" erl -noshell -s perf_couch_skew main 1000000 -s init stop
Queue size: 1000000
12321.649 ms
通过基于 NIF 的二项式堆对 1M 行进行 Unicode 整理:
erlc *.erl; ERL_LIBS="..:$ERL_LIBS" erl -noshell -s perf_merger main 1000000 -s init stop
Queue size: 1000000
15871.965 ms
这很不寻常,我原以为它可能会快 10 倍左右。
我开启了eprof
/fprof
,但是在NIF模块方面用处不大,以下是eprof
所说的突出功能
FUNCTION CALLS % TIME [uS / CALLS]
-------- ----- --- ---- [----------]
merger:new/0 1 0.00 0 [ 0.00]
merger:new/2 1 0.00 0 [ 0.00]
merger:size/1 100002 0.31 19928 [ 0.20]
merger:in/3 100000 3.29 210620 [ 2.11]
erlang:put/2 2000000 6.63 424292 [ 0.21]
merger:out/1 100000 14.35 918834 [ 9.19]
我敢肯定,NIF 实现可以更快,因为我有一个纯 C 实现的 unicode 排序规则,它基于使用动态数组的二进制堆,而且速度要快得多。
$ make
gcc -I/usr/local/Cellar/icu4c/55.1/include -L/usr/local/Cellar/icu4c/55.1/lib min_heap.c collate_json.c kway_merge.c kway_merge_test.c -o output -licui18n -licuuc -licudata
./output
Merging 1 arrays each of size 1000000
mergeKArrays took 84.626ms
具体问题我在这里:
- 由于 NIF 模块中的 Erlang <-> C 通信,预计会减慢多少?在这种情况下,纯 C 和 NIF 实现之间的减速可能是 30 倍或更多
- 哪些工具可用于调试 NIF 相关的减速(如本例)?我尝试使用
perf top
来查看函数调用,最上面的(显示了一些十六进制地址)来自 "beam.smp". - 我应该考虑优化 NIF 的哪些方面?例如:我听说应该尽量减少 Erlang 与 C 之间的数据传输,反之亦然,是否还有更多此类方面需要考虑?
调用 NIF 的开销很小。当 Erlang 运行时加载加载 NIF 的模块时,它会使用模拟器指令修补模块的 beam 代码以调用 NIF。在调用实现 NIF 的 C 函数之前,指令本身仅执行少量设置。这不是导致性能问题的区域。
分析 NIF 与分析任何其他 C/C++ 代码非常相似。从你的 Makefile 来看,你似乎是在 OS X 上开发这段代码。在那个平台上,假设你安装了 XCode,你可以使用 Instruments application with the CPU Samples instrument to see where your code is spending most of its time. On Linux, you can use the callgrind tool of valgrind together with an Erlang emulator built with valgrind support 来衡量你的代码。
如果您在代码中使用这些工具,您会发现,例如,perf_merger:main/1
将大部分时间花在 merger_nif_heap_get
上,而 merger_nif_heap_get
又花费了大量时间在 CollateJSON
。该函数似乎调用了 convertUTF8toUChar
和 createStringFromJSON
很多次。您的 NIF 似乎也执行大量内存分配。这些是您应该关注的领域,以加速您的代码。