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 运行时加载加载 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。该函数似乎调用了 convertUTF8toUCharcreateStringFromJSON 很多次。您的 NIF 似乎也执行大量内存分配。这些是您应该关注的领域,以加速您的代码。