一、概述
下图显示了 SQLite 在标准工作负载上使用的 CPU 周期数,对于大约 10 年前的 SQLite 版本。与旧版本相比,最新版本的 SQLite 使用大约三分之一的 CPU 周期。
本文介绍了 SQLite 开发人员如何测量 CPU 使用率、这些测量值的实际含义,以及 SQLite 开发人员在不断寻求进一步降低 SQLite 库的 CPU 使用率时使用的技术。
使用 gcc 5.4.0 和 -Os 在 x64 上的 Ubuntu 16.04 上使用 cachegrind 进行测量。
2.衡量绩效
简而言之,SQLite 的 CPU 性能测量如下:
- 在交付配置中编译 SQLite,无需任何特殊的遥测或调试选项。
- 将 SQLite 与运行大约 30,000 个代表典型工作负载的 SQL 语句的测试程序相链接。
- 使用cachegrind计算消耗的 CPU 周期数 。
2.1. 编译选项
对于性能测量,SQLite 的编译方式与在生产系统中使用的方式大致相同。编译时配置是“近似的”,因为 SQLite 的每次生产使用都是不同的。一个系统使用的编译时选项不一定与其他系统使用的相同。关键是要避免显着影响生成的机器代码的选项。例如,省略了 -DSQLITE_DEBUG 选项,因为该选项会在 SQLite 库的性能关键部分中间插入数千条 assert() 语句。-pg 选项(在 GCC 上)被省略,因为它会导致编译器发出额外的概率性能测量代码,这会干扰实际的性能测量。
对于性能测量,使用 -Os 选项(针对大小进行优化)而不是 -O2,因为 -O2 选项会产生大量代码移动,因此很难将特定 CPU 指令与 C 源代码行相关联。
2.2. 工作量
“典型”工作负载由 规范 SQLite 源代码树中的speedtest1.c程序生成。该程序力求以一种典型的实际应用程序的方式使用 SQLite 库。当然,每个应用程序都是不同的,因此没有测试程序可以准确反映所有应用程序的行为。
随着 SQLite 开发人员对什么构成“典型”用法的理解的发展,speedtest1.c 程序会不时更新。
speed-check.sh shell 脚本,也在规范的源代码树中,用于运行 speedtest1.c 程序。要复制性能测量,请将以下文件收集到一个目录中:
- “speed-check.sh”脚本,
- “speedtest1.c”测试程序,以及
- SQLite合并源文件“ sqlite3.c”和“sqlite3.h”
然后运行“sh speed-check.sh trunk”。
2.3. 性能测量
Cachegrind用于衡量性能,因为它给出的答案可重复到 7 位或更多位有效数字。相比之下,实际(挂钟)运行时间几乎无法重复超过一位有效数字。
2.4. 微优化
cachegrind 的高可重复性允许 SQLite 开发人员实施和测量“微优化”。微优化是对代码的更改,它会导致非常小的性能提升。典型的微优化将 CPU 周期数减少 0.1% 或 0.05% 甚至更少。这种改进是不可能用现实世界的时间来衡量的。但是成百上千的微优化加起来,导致了可衡量的实际性能提升。
3.绩效衡量工作流程
当 SQLite 开发人员编辑 SQLite 源代码时,他们运行 speed-check.sh shell 脚本来跟踪更改对性能的影响。此脚本编译 speedtest1.c 程序,在 cachegrind 下运行它,使用cg_anno.tcl TCL 脚本处理 cachegrind 输出 ,然后将结果保存在一系列文本文件中。speed-check.sh 脚本的典型输出如下所示:
==8683== ==8683== I refs: 1,060,925,768 ==8683== I1 misses: 23,731,246 ==8683== LLi misses: 5,176 ==8683== I1 miss rate: 2.24% ==8683== LLi miss rate: 0.00% ==8683== ==8683== D refs: 557,686,925 (361,828,925 rd + 195,858,000 wr) ==8683== D1 misses: 5,067,063 ( 3,544,278 rd + 1,522,785 wr) ==8683== LLd misses: 57,958 ( 16,067 rd + 41,891 wr) ==8683== D1 miss rate: 0.9% ( 1.0% + 0.8% ) ==8683== LLd miss rate: 0.0% ( 0.0% + 0.0% ) ==8683== ==8683== LL refs: 28,798,309 ( 27,275,524 rd + 1,522,785 wr) ==8683== LL misses: 63,134 ( 21,243 rd + 41,891 wr) ==8683== LL miss rate: 0.0% ( 0.0% + 0.0% ) text data bss dec hex filename 523044 8240 1976 533260 8230c sqlite3.o 220507 1007870 7769352 sqlite3.c
输出的重要部分(开发人员最关注的部分)以红色显示。基本上,开发人员想知道编译后的 SQLite 库的大小以及运行性能测试需要多少 CPU 周期。
cg_anno.tcl脚本的输出 显示每行代码所花费的 CPU 周期数。该报告大约有 80,000 行长。以下是从报告中间截取的简短片段,以显示其外观:
. SQLITE_PRIVATE int sqlite3BtreeNext(BtCursor *pCur, int *pRes){ . MemPage *pPage; . assert( cursorOwnsBtShared(pCur) ); . assert( pRes!=0 ); . assert( *pRes==0 || *pRes==1 ); . assert( pCur->skipNext==0 || pCur->eState!=CURSOR_VALID ); 369,648 pCur->info.nSize = 0; 369,648 pCur->curFlags &= ~(BTCF_ValidNKey|BTCF_ValidOvfl); 369,648 *pRes = 0; 739,296 if( pCur->eState!=CURSOR_VALID ) return btreeNext(pCur, pRes); 1,473,580 pPage = pCur->apPage[pCur->iPage]; 1,841,975 if( (++pCur->aiIdx[pCur->iPage])>=pPage->nCell ){ 4,340 pCur->aiIdx[pCur->iPage]--; 5,593 return btreeNext(pCur, pRes); . } 728,110 if( pPage->leaf ){ . return SQLITE_OK; . }else{ 3,117 return moveToLeftmost(pCur); . } 721,876 }
当然,左边的数字是该行代码的 CPU 周期数。
cg_anno.tcl 脚本从默认的 cachegrind 注释输出中删除无关的细节,以便可以使用并排差异比较前后报告,以查看微优化尝试如何影响性能的具体细节。
4.限制
使用标准化的 speedtest1.c 工作负载和 cachegrind 显着提高了性能。但是,重要的是要认识到这种方法的局限性:
性能测量是使用单个编译器 (gcc 5.4.0)、优化设置 (-Os) 和单个平台(x64 上的 Ubuntu 16.04 LTS)完成的。其他编译器和处理器的性能可能会有所不同。
正在测量的 speedtest1.c 工作负载试图代表 SQLite 的广泛典型用途。但是每个应用程序都是不同的。speedtest1.c 工作负载可能不能很好地代表某些应用程序执行的各种活动。SQLite 开发人员不断努力改进 speedtest1.c 程序,使其成为实际 SQLite 使用的更好代理。欢迎社区反馈。
cachegrind 提供的循环计数可以很好地代表实际性能,但并非 100% 准确。
此处仅测量 CPU 周期数。CPU 周期数是能源消耗的一个很好的代表,但不一定与现实世界的时间相关。执行 I/O 所花费的时间并没有反映在 CPU 周期计数中,并且 I/O 时间在许多 SQLite 使用场景中占主导地位。