我正在编写一个类似可编程vi编辑器的c库,并计划提供一系列API(总共超过20个):
void vi_dw(struct vi *vi);
void vi_de(struct vi *vi);
void vi_d0(struct vi *vi);
void vi_d$(struct vi *vi);
...
void vi_df(struct vi *, char target);
void vi_dd(struct vi *vi);
这些API不执行核心操作,它们只是包装器。例如,我可以像这样实现vi_de()
:
void vi_de(struct vi *vi){
vi_v(vi); //enter visual mode
vi_e(vi); //press key 'e'
vi_d(vi); //press key 'd'
}
然而,如果包装器像这样简单,我必须编写超过20个类似的包装函数。
因此,考虑实现更复杂的包装器来减少数量:
void vi_d_move(struct vi *vi, vi_move_func_t move){
vi_v(vi);
move(vi);
vi_d(vi);
}
static inline void vi_dw(struct vi *vi){
vi_d_move(vi, vi_w);
}
static inline void vi_de(struct vi *vi){
vi_d_move(vi, vi_e);
}
...
函数
vi_d_move()
是一个更好的包装函数,它可以将一部分相似的移动操作转换为API,但不是全部,比如vi_f()
,需要另一个带有第三个参数char target
的包装函数。我刚刚解释了从我的项目中选择的示例。上面的伪代码比实际情况简单,但足以表明:
包装器越复杂,我们需要的包装器就越少,它们会变得更慢(它们将变得更间接或需要考虑更多条件)。
有两个极端:
- 只使用一个包装器,但足够复杂以适应所有移动操作并将其转换为相应的API。
- 使用二十多个小而简单的包装器。一个包装器是一个API。
对于情况2,这些包装器简单而快速,但驻留在缓存中的机会较少。至少,对于任何首次调用的API,都会发生缓存未命中。(CPU需要从内存中获取指令,而不是L1、L2)。
目前,我实现了五个包装器,它们每个都相对简单和快速。这似乎是一种平衡,但只是表面上的。我选择五个只是因为我觉得移动操作可以自然地分为五组。我不知道如何评估它,我不是指分析器,我是指在理论上,在这种情况下应考虑哪些主要因素?
在文章的结尾,我想为这些API添加更多细节:
- 这些API需要快速。因为这个库被设计为高性能的虚拟编辑器。删除/复制/粘贴操作旨在接近裸的C代码。
- 基于此库的用户程序很少调用所有这些API,只调用它们的部分,并且通常每个API最多不超过10次。
- 在实际情况中,这些简单包装器的大小大约为每个80字节,即使合并成一个单独的复杂包装器,也不会超过160字节。(但会引入更多的if-else分支)。
lua-shell
为例(有点离题,但有些朋友想知道我为什么如此关心其性能):
lua-shell
是一个使用lua
作为脚本的*nix shell。它的命令执行单元(执行forks(),execute()..)只是一个注册到lua状态机中的C模块。
Lua-shell
将所有内容视为lua
。因此,当用户输入时:
local files = `ls -la`
然后按下Enter
键。字符串输入首先被发送到lua-shell的预处理器——该预处理器将混合语法转换为纯lua代码:
local file = run_command("ls -la")
run_command()
是lua-shell命令执行单元的入口,它是一个C模块,我之前已经提到过。
现在我们可以谈谈libvi
了。lua-shell的预处理器是我正在编写的库的第一个用户。下面是相关的代码(伪代码):
#include"vi.h"
vi_loadstr("local files = `ls -la`");
vi_f(vi, '`');
vi_x(vi);
vi_i(vi, "run_command(\"");
vi_f(vi, '`');
vi_x(vi);
vi_a(" \") ");
上面的代码是luashell预处理器实现的部分。在生成纯Lua代码后,他将其提供给Lua状态机并运行。
shell用户对
Enter
和新提示之间的时间间隔非常敏感,在大多数情况下,lua-shell需要具有更大尺寸和更复杂混合语法的预处理脚本。这是使用
libvi
的典型情况。
void vi_dw(struct vi *vi);
明显是C语言,如果是C++的话应该是void vi::dw( )
。 - MSalters