Measuring and Reducing CPU Usage in SQLite
Measuring and Reducing CPU Usage in SQLite
1.概述
2.衡量绩效
2.1.编译选项
2.2.工作量
2.3.性能测量
2.4.Microoptimizations
3.性能测量工作流程
4.限制
1.概述
下面的图表显示了SQLite在标准工作负载下所使用的CPU周期数,对于所有版本的SQLite而言,这些周期大约在9年前。最近的版本使SQLite与旧版本相比使用了不到三分之一的CPU周期。
本文描述了SQLite开发人员如何衡量CPU使用情况,这些度量实际意味着什么,以及SQLite开发人员如何继续探索进一步减少SQLite库的CPU使用率的技术。
使用gcc 5.4.0和-Os在x64上的Ubuntu 16.04上使用cachegrind进行测量。
归一化值使3.21.0版本为100%。
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库。当然,每个应用程序都是不同的,所以没有一个测试程序可以准确地反映所有应用程序的行为。
speedtest1.c程序会不时更新,因为SQLite开发人员对构成“典型”使用情况的理解有所发展。
该speed-check.sh shell脚本,同样在经典的源代码树,用来运行程序speedtest1.c。要复制性能测量,请将以下文件收集到一个目录中:
- “speed-check.sh”脚本,
- the "speedtest1.c" test program, and
- SQLite合并源文件“sqlite3.c”和“sqlite3.h”
然后运行“sh speed-check.sh trunk”。
2.3.性能测量
Cachegrind用于衡量性能,因为它提供了可重复的7个或更多有效数字的答案。相比之下,实际(挂钟)运行时间几乎不可重复超过一位有效数字。
2.4.Microoptimizations
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脚本的典型输出如下所示:
==16429== I refs: 1,291,005,499
==16429== I1 misses: 24,688,182
==16429== LLi misses: 5,027
==16429== I1 miss rate: 1.91%
==16429== LLi miss rate: 0.00%
==16429==
==16429== D refs: 663,242,182 (418,445,823 rd + 244,796,359 wr)
==16429== D1 misses: 5,958,032 ( 3,902,273 rd + 2,055,759 wr)
==16429== LLd misses: 45,636 ( 14,803 rd + 30,833 wr)
==16429== D1 miss rate: 0.8% ( 0.9% + 0.8% )
==16429== LLd miss rate: 0.0% ( 0.0% + 0.0% )
==16429==
==16429== LL refs: 30,646,214 ( 28,590,455 rd + 2,055,759 wr)
==16429== LL misses: 50,663 ( 19,830 rd + 30,833 wr)
==16429== LL miss rate: 0.0% ( 0.0% + 0.0% )
text data bss dec hex filename
466711 6256 1864 474831 73ecf sqlite3.o
199979 914462 7026217 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批注输出中删除无关细节,以便可以使用并排diff比较前后报告,以查看微优化尝试如何影响性能的具体细节。
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周期数中,而且在很多SQLite使用场景中,I / O时间占主导地位。
SQLite在公共领域。