测量和减少 SQLite 中的 CPU 使用率

、概述

下图显示了 SQLite 在标准工作负载上使用的 CPU 周期数,对于大约 10 年前的 SQLite 版本。与旧版本相比,最新版本的 SQLite 使用大约三分之一的 CPU 周期。

本文介绍了 SQLite 开发人员如何测量 CPU 使用率、这些测量值的实际含义,以及 SQLite 开发人员在不断寻求进一步降低 SQLite 库的 CPU 使用率时使用的技术。



使用 gcc 5.4.0 和 -Os 在 x64 上的 Ubuntu 16.04 上使用 cachegrind 进行测量。

2.衡量绩效

简而言之,SQLite 的 CPU 性能测量如下:

  1. 在交付配置中编译 SQLite,无需任何特殊的遥测或调试选项。
  2. 将 SQLite 与运行大约 30,000 个代表典型工作负载的 SQL 语句的测试程序相链接。
  3. 使用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 使用场景中占主导地位。