小巧。快速。可靠。
三选二。
测量和降低 SQLite 中的 CPU 使用率

1. 概述

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

本文介绍了 SQLite 开发人员如何测量 CPU 使用率、这些测量结果的实际含义以及 SQLite 开发人员在其持续改进 SQLite 库 CPU 使用率的努力中所采用的技术。



使用 Ubuntu 16.04 上的 x64 架构、gcc 5.4.0 编译器和 -Os 优化选项,通过 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 程序。要复制性能测量结果,请将以下文件收集到一个目录中:

然后运行 "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 已实现了显著的性能改进。但是,必须认识到这种方法的局限性:

此页面上次修改于 2022-01-08 05:02:57 UTC