coost-A tiny boost library in C++11
coost 是一个兼具性能与易用性的跨平台 C++ 基础库,原名为 co,后改为 cocoyaxi,前者过短,后者过长,取中庸之道,又改为 coost。
为什么叫 coost 呢?以前有朋友称之为小型 boost 库,比 boost 小一点,那就叫 coost 好了。它有多小呢?在 linux 与 mac 上编译出来的静态库仅 1M 左右大小。虽然小,却提供了足够强大的功能:
- 命令行参数与配置文件解析库(flag)
- 高性能日志库(log)
- 单元测试框架(unitest)
- go-style 协程
- 基于协程的网络编程框架
- 高效 JSON 库
- 基于 JSON 的 RPC 框架
- 面向玄学编程
- 原子操作(atomic)
- 随机数生成器(random)
- 高效字符流(fastream)
- 高效字符串(fastring)
- 字符串操作(str)
- 时间库(time)
- 线程库(thread)
- 定时任务调度器
- 高性能内存分配器
- LruMap
- hash 库
- path 库
- 文件系统操作(fs)
- 系统操作(os)
本次发布的版本,直接从 v2.0.3 跳到了 v3.0.0,跨度非常之大,它在性能、易用性、稳定性等方面均有全面的提升。
性能优化
内存分配器
v3.0 中实现了一个新的内存分配器(co/malloc),它不是通用的内存分配器,在 free
与 realloc
时,需要额外带上原内存块的 size 信息,这在使用时可能有一点点不便,但可以简化设计,提升内存分配器的性能。
通用的内存分配器,像 ptmalloc, jemalloc, tcmalloc 以及 mimalloc 等,小内存释放后大概率会被它们缓存住,而并没有归还给操作系统,这可能造成释放大量小内存后内存占用量却始终不降的疑似内存泄漏的现象。co/malloc 对此做了优化,在兼顾性能的同时,会尽可能多的将释放的内存归还给操作系统,有利于降低程序的内存占用量,在实测中也取得了良好的效果。
co/malloc 还提供了 co::stl_allocator,可以替换 STL 容器中默认的分配器 std::allocator。co/stl.h 中提供了一些常用的替换 allocator 后的容器,与 std 版本相比有着性能上的优势。
co/malloc 已经成为 coost 内部使用的默认内存分配器,像 fastring、fastream、Json 等均基于 co/malloc 实现。co/test 中提供了简单的测试代码,可以执行如下命令编译及运行:
xmake b mem
xmake r mem -t 4 -s
-t
指定线程数量,-s
表示与系统的内存分配器进行对比。下面是在不同系统中的测试结果(4线程):
os/cpu | co::alloc | co::free | ::malloc | ::free | speedup |
---|---|---|---|---|---|
win/AMD 3.2G | 7.32 | 6.83 | 86.05 | 105.06 | 11.7/15.3 |
mac/i7 2.4G | 9.91 | 9.86 | 55.64 | 60.20 | 5.6/6.1 |
linux/i7 2.2G | 10.80 | 7.51 | 1070.5 | 21.17 | 99.1/2.8 |
上表中,时间单位为纳秒(ns),linux 是在 Windows WSL 中运行的 ubuntu 系统,speedup 是 co/malloc 相对于系统内存分配器的性能提升倍数。可以看到 co::alloc 在 Linux 上比 ::malloc 提升了近 99 倍,这其中的一个重要原因是 ptmalloc 在多线程环境中锁竞争开销较大,而 co/malloc 在设计上尽可能避免了锁的使用,小块内存的分配、释放不需要锁,甚至在跨线程释放时连自旋锁也不用。
原子操作
v3.0 中,原子操作支持 memory order,以满足一些高性能应用场景的需求。co/atomic.h 中定义了 6 种 memory order,与 C++11 标准中保持一致:
mo_relaxedmo_consumemo_acquire
mo_releasemo_acq_relmo_seq_cst
- 代码示例
int i = 0;
uint64 u = 0;
atomic_inc(&i, mo_relaxed);
atomic_load(&i, mo_relaxed);
atomic_add(&u, 3); // mo_seq_cst
atomic_add(&u, 7, mo_acquire);
易用性提升
简化初始化过程
v2.0.3 中,main 函数可能得像下面这样写:
#include "co/flag.h"
#include "co/log.h"
#include "co/co.h"
int main(int argc, char** argv) {
flag::init(argc, argv);
log::init();
co::init();
// do something here...
co::exit();
return 0;
}
为了提升用户的使用体验,v3.0 中移除了 log::init()
、co::init()
、co::exit()
等 API,现在 main 函数可以写成下面这样:
#include "co/flag.h"
int main(int argc, char** argv) {
flag::init(argc, argv);
// do something here...
return 0;
}
v3.0 中,整个 coost 库唯一需要调用的初始化接口就是 flag::init()
,该接口用于解析命令行参数及配置文件。
flag
co/flag 是一个简单易用的命令行参数与配置文件解析库,coost 中的日志、协程、RPC框架等组件会用它定义配置项。
v3.0 中,对一些细节处进行了改进,如 --help 只显示用户自己定义的 flag,而不会显示 coost 内部定义的 flag。
v3.0 还新增了 flag 别名功能,在定义 flag 时,可以指定任意数量的别名:
DEF_bool(debug, false, ""); // no alias
DEF_bool(debug, false, "", d);// d is an alias of debug
DEF_bool(debug, false, "", d, dbg); // 2 aliases
日志
v2.0.3 中,co/log 提供类似 google glog 的日志功能,将日志分成 debug、info、warning、error、fatal等级别。v3.0 中,新增 TLOG,即按 topic 分类的日志,不同 topic 的日志输出到不同的文件。
v3.0 中,co/log 不需要用户手动初始化,包含 co/log.h 即可使用,不需要特别的设置。
#include "co/log.h"
int main(int argc, char** argv) {
flag::init(argc, argv);
DLOG << "hello " << 23; // debug
LOG << "hello " << 23;// info
WLOG << "hello " << 23; // warning
ELOG << "hello " << 23; // error
FLOG << "hello " << 23; // fatal
TLOG("rpc") << "hello " << 23;
TLOG("xxx") << "hello " << 23;
return 0;
}
JSON
v2.0.3 中,JSON 对象构建在连续内存上,可以减少内存分配、提升性能,但易用性会降低。v3.0 中有了 co/malloc,JSON 采用了更加灵活的实现方式,在保持高性能的同时,易用性方面也有了质的提升。
co/json 与 rapidjson 的性能比较
os | co/json stringify | co/json parse | rapidjson stringify | rapidjson parse | speedup |
---|---|---|---|---|---|
win | 569 | 924 | 2089 | 2495 | 3.6/2.7 |
mac | 783 | 1097 | 1289 | 1658 | 1.6/1.5 |
linux | 468 | 764 | 1359 | 1070 | 2.9/1.4 |
上表是将 twitter.json 最小化后的测试结果,耗时单位为微秒(us),speedup 是 co/json 相对于 rapidjson 的性能提升倍数。
v3.0 中实现了 Json 类,它采用流畅(fluent)接口设计,用起来更加方便。
// {"a":23,"b":false,"s":"123","v":[1,2,3],"o":{"xx":0}}
Json x = {
{ "a", 23 },
{ "b", false },
{ "s", "123" },
{ "v", {1,2,3} },
{ "o", {
{"xx", 0}
}},
};
// equal to x
Json y = Json()
.add_member("a", 23)
.add_member("b", false)
.add_member("s", "123")
.add_member("v", Json().push_back(1).push_back(2).push_back(3))
.add_member("o", Json().add_member("xx", 0));
x.get("a").as_int(); // 23
x.get("s").as_string();// "123"
x.get("s").as_int(); // 123, string -> int
x.get("v", 0).as_int();// 1
x.get("v", 2).as_int();// 3
x.get("o", "xx").as_int(); // 0
x["a"] == 23;// true
x["s"] == "123"; // true
x.get("o", "xx") != 0; // false
RPC
v3.0 中,RPC 框架新增对 HTTP 协议的支持,将 RPC 服务与 HTTP 服务融为一体。
#include "co/all.h"
int main(int argc, char** argv) {
flag::init(argc, argv);
rpc::Server()
.add_service(new xx::HelloWorldImpl)
.start("127.0.0.1", 7788, "/xx");
for (;;) sleep::sec(80000);
return 0;
}
rpc::Server
可以添加多个 service,调用 start()
方法即可启动 RPC 服务,该方法的第 3 个参数指定 HTTP 服务的 URL。用户可以通过 rpc::Client
调用其服务,也可以通过 HTTP 的方式调用其服务,如:
curl http://127.0.0.1:7788/xx --request POST --data '{"api":"ping"}'
curl http://127.0.0.1:7788/xx --request POST --data '{"api":"HelloWorld.hello"}'
RPC 请求或 HTTP 请求的 body 部分是 JSON 字符串,需要用 api 字段指明调用的方法,该字段的值一般为 service.method 形式。ping 则是 coost RPC 框架内置的特殊 service,可用于发送心跳或测试。
稳定性提升
coost 提供了一套简单易用的单元测试框架,并且在 unitest 目录下提供了大量的单元测试代码,几乎覆盖了 coost 内部的所有组件,为 coost 的稳定性提供了重要的保障。
v3.0 中又新增了不少单元测试代码,进一步提高了单元测试覆盖率。另外,针对一些不便编写单元测试的功能,coost 在 test 目录下单独提供了大量的测试代码。