├── .github └── workflows │ └── update-chirpy.yml ├── 2012-03-16-vim_clang_complete.md ├── 2015-12-07-high_performance_hash_table.md ├── 2016-05-13-hashtable_with_2hash.md ├── 2016-05-21-linux_net_parallel.md ├── 2016-05-29-percentile_probability_estimate.md ├── 2016-06-05-use_gtestx_for_benchmark.md ├── 2017-02-04-shm_container_intro.md └── res ├── 201512-high_performance_hash_table ├── array_usage_vs_rand_writes.png ├── effective_rows.png ├── fix_ht_usage_vs_rows.png ├── ht_250w_ratio60_rows.png └── ht_usage_depth_vs_ratio.png ├── 201605-hashtable_with_2hash ├── linked_ht_1vs2_avg_depth.png └── linked_ht_1vs2_max_depth.png ├── 201605-linux_net_parallel ├── linux_net_irqsched.png ├── linux_net_irqsched2.png ├── linux_net_multiqueue.png ├── linux_net_overview.png ├── linux_net_reuseport.png ├── linux_net_reuseport2.png ├── linux_net_rfs.png └── linux_net_rps.png ├── 201605-percentile_probability_estimate └── max_pdf_vs_orig_cdf.png └── 201702-shm_container_intro └── shm_container_arch.png /.github/workflows/update-chirpy.yml: -------------------------------------------------------------------------------- 1 | name: "Update chirpy branch" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - .gitignore 8 | - README.md 9 | - LICENSE 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | permissions: 15 | contents: write 16 | pages: write 17 | id-token: write 18 | 19 | # Allow one concurrent deployment 20 | concurrency: 21 | group: "update-chirpy" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | push-update: 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - name: Checkout-master 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token. 34 | # submodules: true 35 | # If using the 'assets' git submodule from Chirpy Starter, uncomment above 36 | # (See: https://github.com/cotes2020/chirpy-starter/tree/main/assets) 37 | 38 | - name: Update-chirpy-branch 39 | run: | 40 | git switch chirpy 41 | git config --local user.email "mikewei@126.com" 42 | git config --local user.name "mikewei" 43 | git merge -m "merge master" --log master 44 | rm -f chirpy/_posts/*.md && cp *.md chirpy/_posts/ 45 | rm -rf chirpy/res/ && cp -r ./res chirpy/ 46 | git diff --quiet || git commit -a -m "update blogs" 47 | 48 | - name: Push-chirpy-branch 49 | uses: ad-m/github-push-action@master 50 | with: 51 | github_token: ${{ secrets.BLOGS_ACTIONS_PAT}} 52 | branch: chirpy 53 | -------------------------------------------------------------------------------- /2012-03-16-vim_clang_complete.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vim开发辅助插件clang_complete 3 | date: 2012-03-16 18:00:00 +0800 4 | categories: [tools] 5 | tags: [vim] 6 | math: false 7 | --- 8 | clang_complete号称是目前最强的vim上的语法自动补齐插件,其实现基于clang,一个强大的编译系统,动态地将c/c++文件解析成语法树,然后基于语法树做补齐,比其它实现更为准确 9 | 10 | debian下安装过程: 11 | 12 | sudo apt-get install clang #安装clang, 主要是一些命令行工具 13 | sudo apt-get install libclang-dev #安装libclang, 底层库, 插件直接使用库更高效 14 | sudo apt-get install vim-nox #安装支持python的vim 15 | 16 | 这里下载vim[clang_complete插件](https://www.vim.org/scripts/script.php?script_id=3302) 17 | 18 | vim clang_complete.vmb -c 'so %' -c 'q' #安装vim插件 19 | 20 | 修改~/.vimrc 21 | 22 | let g:clang_use_library=1 "这个选项不开有问题,原因未知 23 | let g:clang_auto_select=1 "自动高亮菜单第一个,更方便 24 | 25 | 试用中... 26 | 27 | -------------------------------------------------------------------------------- /2015-12-07-high_performance_hash_table.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 一种高性能多阶哈希表实现 3 | date: 2015-12-07 21:45:00 +0800 4 | categories: [algorithm] 5 | tags: [hash] 6 | math: false 7 | --- 8 | ## 概述 9 | 10 | 本文提出一种改进版的多阶哈希表实现 -- 等比多阶哈希表,相对于当前服务器后台所常用的多阶哈希表,在空间利用率类似的情况下,可将有效查询的性能提升约10倍,空查询的性能约提升约3~4倍。 11 | 12 | ## 背景 13 | 14 | 多阶哈希表由于其结构简单稳定、易于在共享内存中实现等优点在服务器开发中广泛采用。目前常用的多阶哈希表,由一个近似R行C列的矩阵式数组来实现,多行用于通过多次哈希来解决哈希冲突。每一行的哈希算法为`Key % 质数(C,r)`,其中`r`为当前行号,`质数(C,r)`为小于C的第r个质数。 15 | 16 | 查找与插入操作的示例伪代码为: 17 | 18 | bool HashTableFind(Key k) 19 | { 20 | for (int r = 0; r < R; r++) { 21 | int c = k % GetPrime(r); 22 | if (GetKey(r, c) == k) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } 28 | 29 | bool HashTableInsert(Key k) 30 | { 31 | if (HashTableFind(key)) 32 | return false; 33 | for (int r = 0; r < R; r++) { 34 | int c = k % GetPrime(r); 35 | if (IsHashNodeEmpty(r, c)) { 36 | SetHashNode(r, c, k); 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | 该哈希表算法的一个关键参数为`R`(行数),R数的取值影响查询效率和空间利用率。查询效率与R成反比,因为有效查询的平均比较次数为`R/2`,空查询的比较次数为`R`,R越大查询效率越低。空间利用率与R成正比,关系如下图(x轴为R取值,y轴为空间利用率): 44 | 45 | ![row_vs_usage](/res/201512-high_performance_hash_table/fix_ht_usage_vs_rows.png) 46 | 47 | 一个广泛应用的R的经验值是50,此时空间利用率在90%以上,(有效查询)平均比较次数25次,最大(空查询)比较次数50次,单核平均有效查询性能约`200+万/秒`,空查询`100+万/秒`。这个性能数据在高并发场景下并不算特别理想,设想服务器每个请求需做100次哈希表查询,则单核每秒最多处理不到2万次请求。对比std::unordered_map等开链式哈希表实现(查询性能接近1000万/秒),多阶哈希表性能相比也偏低。基于本实现一种简单的提升查询效率的方式是减少阶数(须同时增大列数),但会以大幅降低空间利用率为代价。 48 | 49 | ## 改进设计 50 | 51 | 改进的核心思想是让更多的哈希节点集中于哈希表的头几行,以减小平均节点深度,从而缩小查询操作时的平均比较次数。另外一个需要考虑的因素是需要保证空间的利用率,特别是占有大量节点的头几行如果利用率不高,则整体利用率会受较大影响。我们先以单行节点为例分析,如何保证利用率?通过简单的概率分析可知,单行节点占用率(即非空节点数比上单行总节点数)与该行上随机写入的次数成非线性的单调递增关系,如下图所示: 52 | 53 | ![row_vs_usage](/res/201512-high_performance_hash_table/array_usage_vs_rand_writes.png) 54 | 55 | 从上图可以看出在近似随机写入的情况下,`写入数/单行节点数`决定了该行的占用率,如随机写入1倍节点数时,节点占用率约在60%多,2倍节点数时达到80%多,随着写入数的继续增加节点占用率逐渐缓慢增长逼近100%。从这里我们可得一个思路,如果能保证多阶哈希被填满时,其中每一阶(行)的写入次数/该行节点数大于某个系数(如大于2),即可保证该行的利用率达到某一比例(如大于80%)。同时注意到,在多阶哈希表的结构中,每一阶(行)写入次数实际就是该阶(行)及以下所有阶(行)的节点总数。由此可得,保证`单阶节点数/该阶及以下阶总节点数`大于某个常数,一个简单直接的方式就是构造一个多阶哈希表,唯一与原来不同的是,各阶节点数近似组成一个系数为`ratio (0 /proc/irq/123/smp_affinity 63 | 64 | 65 | #### 多队列网卡 66 | 67 | 如前所述,传统的单网卡默认无法充分利用多核,即使是多网卡,在数量小于核数的情况下也是一样。于是产生了在今天广泛使用的多队列网卡(Multi-Queue NIC),在一些资料里这种技术也叫RSS(Receive-Side Scaling)。这种技术概括来说是从硬中断的层面支持了单网卡IO的并行,其工作原理如下图所示: 68 | 69 | ![multiqueue](/res/201605-linux_net_parallel/linux_net_multiqueue.png) 70 | 71 | 多队列网卡通过引入RX-Queue的机制,将输入流量水平分到多个“虚拟的网卡”也是RX-Queue,每个RX-Queue像一个独立设备一样有自己的中断号并可以独立并行地工作。RX-Queue的数量一般可以配置为与核数一致,这样可以充分利用多核资源。 72 | 73 | 值得注意的是,输入流量拆分到RX-Queue的算法是根据Hash(SrcIP, SrcPort, DstIP, DstPort)来计算的(可以试想下为什么是用hash而不是类似随机分发?对,主要是为了避免乱序)。如果出现大部分流量来自少量IP:Port的场景,多队列网卡并就爱莫能助了。 74 | 75 | 你可以使用前面提到的interrupts文件来观察多队列网卡的分发效果: 76 | 77 | cat /proc/interrupts 78 | 79 | 这里也[有篇文章](https://blog.csdn.net/turkeyzhou/article/details/7528182)较详细介绍了这个主题可作参考。 80 | 81 | 82 | #### RPS 83 | 84 | 在没有多队列网卡的服务器上,比如一个典型的场景是虚拟机或云主机,如何优化网络IO呢?下面要介绍的是纯软件的优化方案:RPS & RFS, 这是在2.6.35内核加入的由Google工程师Tom Herbert开发的优化补丁。它的工作原理如下图所示: 85 | 86 | ![multiqueue](/res/201605-linux_net_parallel/linux_net_rps.png) 87 | 88 | RPS是工作在NAPI层(或者说在Soft-IRQ处理中接近入口的位置)的入流量分发机制,它利用了前面提到的Per-CPU的backlog队列来将数据包分发到目标core上。默认的分发算法与多队列机制类似,也是使用IP,Port四元组的哈希来映射到某一个core。与多队列机制类似,若是流量来自少量IP:Port的场景,负载将无法很好地均衡在多核上。我们目前在AWS虚拟机上普遍配置启用了RPS,优化效果还是非常明显。 89 | 90 | 配置RPS的方法也很简单: 91 | 92 | echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus 93 | 94 | 更详细的配置说明可[参考这里](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Performance_Tuning_Guide/network-rps.html) 95 | 96 | 如何观察RPS的分发效果呢?由于RPS分发会多做一次Soft-IRQ调度,我们可以通过观察Soft-IRQ的统计接口来观察调度效果: 97 | 98 | cat /proc/softirqs | grep NET_RX 99 | 100 | 前面我们说到在没有多队列网卡的服务器,RPS可以发挥重要作用,那如果已经有多队列网卡了是否还需要RPS呢?根据我目前的经验来说,一般情况下有队列网卡的环境下配置RPS不会再有明显的提升。但我认为仍存在一些情况结合RPS是有意义的,比如队列数明显少于核数,再比如某些RFS(下面会介绍)可以优化的场景可以打开RPS+RFS。 101 | 102 | 如果你有兴趣看一下RPS的关键内核代码,可以[查看这里](https://lxr.free-electrons.com/source/net/core/dev.c#L4202)。 103 | 104 | 这里也[有篇文章](https://simohayha.iteye.com/blog/720850)介绍了一些内核实现细节可作参考。 105 | 106 | #### RFS 107 | 108 | RFS是在RPS分发机制基础上的一个扩展。它尝试解决这么一个问题,即然我在软件层面做数据包的分发,能不能比硬件多队列方案的近似随机的Hash分发方式更智能更高效一些呢?比如按Hash分发的一个问题就是,准备接收这个数据包的进程所在core很可能跟按Hash选择的core不是同一个,这样会导致cache miss及cache line bouncing,在多核高并发场景这对性能影响会十分可观。 109 | 110 | RFS尝试优化这个问题,它尽力将收到数据包分发给接收它的进程所在的core上,先看一下原理图: 111 | 112 | ![rfs](/res/201605-linux_net_parallel/linux_net_rfs.png) 113 | 114 | 首先RFS会维护一张全局的路由表(图中SockFlowTable),表中记录了一个FlowHash(四元组的Hash值)到对应CPU核的路由项。表项怎么建立呢?是在进程调用某socket的recvmsg系统调用(也包括recv/recvfrom)时,将该socket的FlowHash值(由最后一次收到包的FlowHash决定)与当前的CPU核关联起来。在RPS做包转发时,实际它会先判断是否启用了RFS,并且能找到有效的RFS路由项,否则的话仍使用默认RPS逻缉进行转发。 115 | 116 | 另外RFS还维护一张Per-Queue的局部路由表(图中Per-Queue FlowTable),它有什么用呢?主要作用是为了在全局路由表发生变化时,避免原路由路径上的包还没有被完全处理完而导致的乱序。它的原理并不复杂,在局部路由表中会记录某FlowHash(实际实现是FlowHash的hash)最后一次包转发时的关联CPU核,同时会记录当时该核对应的backlog队列的队尾标号(qtail)。当下次转发该flow上下一个包时,如果全局路由给出的CPU核发生了变化,则判断当前backlog队列的队首标号是否大于qtail,如果是说明上一次转发的包已经被处理完了,可以安全地切换到全局路由给出的新的CPU核,否则的话为了保证有序仍选择上一次使用的CPU核。 117 | 118 | 从原理上可以看出,在每个socket有唯一的进程/线程处理时RFS会有较好地效果,同时,对于在同一进程/线程内多路复用操作多个socket的场景,建议结合绑定进程/线程到固定CPU核的方式可以进一步发挥RFS的作用(让转发路由规则固定,进一步减少cache line bouncing)。 119 | 120 | RFS的配置也比较简单,有两处,一个是全局路由表的大小,另一个是局部路由表的大小(一般设为前者大小/RX-Queue数),如下例: 121 | 122 | echo 32768 > /proc/sys/net/core/rps_sock_flow_entries 123 | echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt 124 | 125 | 更详细的配置说明可[参考这里](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Performance_Tuning_Guide/network-rfs.html)。 126 | 127 | 如果有兴趣看一下它的实现,可从[这里(记录路由)](https://lxr.free-electrons.com/source/net/ipv4/af_inet.c#L762)和[这里(查询路由)](https://lxr.free-electrons.com/source/net/core/dev.c#L3481)入手。 128 | 129 | 这里也[有篇文章](https://www.pagefault.info/?p=115)介绍了实现可供参考。 130 | 131 | 132 | #### XPS 133 | 134 | 作者也是Google的Tom Herbert,内核2.6.38被引入。XPS解决的是一个在多队列网卡场景下才存在的问题:默认情况下当协议栈处理到需要向一个网卡设备发包时,如果是多队列网卡(有多个TX-Queue),会使用四元组hash的方式选择一个TX-Queue进行发送。这里有一个性能损耗是,在多核的场景下,可能会存多个核同时向一个TX-Queue发送数据的情况,因为这个操作需要写相应的tx\_ring等内存,会引发cache line bouncing的问题,带来系统整体性能的下降。而XPS提供这样一种机制,可以将不同的TX-Queue固定地分配给不同的CPU集合去操作,这样对于某一个TX-Queue,仅有一个或少数几个CPU核会去写,可以避免或大大减少冲突写带来的cache line bouncing问题。 135 | 136 | 设置XPS非常简单,与RPS类似,如下示例: 137 | 138 | echo 11 > /sys/class/net/eth0/queues/tx-0/xps_cpus 139 | echo 22 > /sys/class/net/eth0/queues/tx-1/xps_cpus 140 | echo 44 > /sys/class/net/eth0/queues/tx-2/xps_cpus 141 | echo 88 > /sys/class/net/eth0/queues/tx-3/xps_cpus 142 | 143 | 可以注意的是,根据原理,对于非多队列网卡设置XPS是没有意义和效果的。如果一个CPU核没有出现在任何一个TX-Queue的xps\_cpus设置里,当该CPU核对该设备发包时,会退回使用默认hash的方式去选择TX-Queue。 144 | 145 | 如果你对它的实现有兴趣,可以从[这里看起](https://lxr.free-electrons.com/source/net/core/dev.c#L3203)。 146 | 147 | 这里是一篇[原作者对此的简介](https://lwn.net/Articles/412062/)。 148 | 149 | 150 | #### SO_REUSEPORT 151 | 152 | 前面我们讨论的都是协议栈偏底层的并行优化,然而在上层也就是socket层,同样有一个重要的优化补丁:SO\_REUSEPORT socket选项(注意不要与SO\_REUSEADDR搞混)。它的作者还是Tom Herbert~(本文应感谢该伙计:),在内核3.9被引入。 153 | 154 | 它解决了什么样的问题呢?考虑一个监听唯一端口的TCP服务器,如果想利用多核并发以提升总体吞吐,需要考虑使用多进程/多线程。一个简单直接的方法是多个进程竞争accept监听socket,你可能有经验这种方法的一个缺陷是,各个进程/线程无法保证负载均衡地accept到新socket。直接解决这个问题可能需要写比较麻烦的workaround(比如我们曾使用连接数表+sched\_yield的方法来保证负载均衡)。还有一个流行的处理模式是,使用一个线程负责listen和accept,然后将socket负载均衡地dispatch到一个worker线程组,每个worker线程处理一个socket子集的IO。这种模式对于长连接服务还是比较适合的,但如果是有大量connect请求的短连接场景,单个线程accept将可能成为瓶颈。解决这个问题可能又需要考虑使用复杂的多线程竞争accept的方式,但依然有socket访问竞争、cache line bouncing等效率问题。 155 | 156 | 对于UDP服务器,也有类似的问题,单进程读容易达到单核瓶颈,多进程竞争读又会有一定的性能损耗。多进程竞争读的原理如下图所示: 157 | 158 | ![reuseport](/res/201605-linux_net_parallel/linux_net_reuseport.png) 159 | 160 | SO\_REUSEPORT很好地解决了多进程读写同一端口场景的2个问题:负载均衡和访问竞争。通过这个选项,多个用户进程/线程可以各自创建一个独立socket,但它们又共享同一端口,该端口的流量默认按四元组hash的方式分发各socket上(最新内核还支持使用bpf方式自定义分发策略),思路是不是非常熟悉。原理示意图如下: 161 | 162 | ![reuseport](/res/201605-linux_net_parallel/linux_net_reuseport2.png) 163 | 164 | 使用此方式,TCP/UDP服务器编程模式都非常简单了,多进程/线程创建socket设置SO\_REUSEPORT后bind,后面像单进程一样处理就可以了。同时性能也可获得明显提升,我们较早前一个经验是UDP改造后qps提升一倍。 165 | 166 | 167 | 如果你对它的实现有兴趣,可以从[这里(UDP)](https://lxr.free-electrons.com/source/net/ipv4/udp.c#L614)和[这里(TCP)](https://lxr.free-electrons.com/source/net/ipv4/inet_hashtables.c#L238)看看源码。 168 | 169 | 这里有一篇介绍得也比较详细的[文章](https://lwn.net/Articles/542629/)。 170 | 171 | #### 总结 172 | 173 | 本文介绍了Linux内核关于网络IO并行化的一系列技术(多队列、RPS、RFS、XPS、SO\_REUSEPORT),它们是在协议栈不同的层面,但都使用了类似的方法提升了网络IO的并行性,并尽量减少了cache line bouncing。这些出色的工具可以帮助我们在任何Linux平台上构建高性能的网络服务器。 174 | 175 | 176 | -------------------------------------------------------------------------------- /2016-05-29-percentile_probability_estimate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 一种基于概率估计的快速percentile算法 3 | date: 2016-05-29 17:40:00 +0800 4 | categories: [algorithm] 5 | tags: [monitor] 6 | math: true 7 | --- 8 | 计算percentile在后台服务中挺常见的,一个最典型场景就是用于监控系统中计算请求延时(latency)的percentile值,常用的比如有99.9%分位延时、99%分位延时。这个问题在计算机科学里叫[“选择问题”](https://en.wikipedia.org/wiki/Selection_algorithm)。 9 | 10 | 这个问题有许多常见的解法,比如局部排序算法、分区算法、分桶算法。局部排序算法(partial selection sort),简单来说就是建立一个最小堆,堆的大小能覆盖关心分位内的数据,它的时间复杂度O(nlog(k))(n为统计数量、k为堆大小),空间复杂度为O(k);分区算法(partition-based selection),如上文中介绍的[QuickSelect](https://en.wikipedia.org/wiki/Quickselect),时间复杂度O(n),空间复杂度O(n);分桶算法是一个工程上常用的优化方法,按延时值分桶,损失精度以换取速度和简单,时间复杂度O(n)(极快),空间复杂度O(m)(m为分桶数)。 11 | 12 | 上述这些方案在一些场景上都有一些缺点,就是实现偏复杂,要么需要较多地计算,要么需要较多的内存,在一些对精度要求不是特别高的场景下,有点杀鸡用牛刀的感觉。有没有一种更简洁有效的方法呢?我对此问题做了些思考,找到一种基于概率估计的方法。 13 | 14 | 这个方法的思路很简单,就是计算n个统计量的最大值(Max),然后以此值作为对percentile对应值的估计,时间和算法复杂度都很底。思路是比较简单,是否真的靠谱,n值应该怎么来定,还得需要严谨的分析。 15 | 16 | ## 数学分析 17 | 18 | 首先我们把延时值的概率分布假设为[指数分布](https://en.wikipedia.org/wiki/Exponential_distribution)来进行建模。近似为指数分布是合情理的,特别是,往往高延时是由少部分的请求组成(并且后面我们也会看到此建模的结果对非指数分布也基本适用)。 19 | 20 | 我们先回顾下,指数分布的概率密度函数和累积分布函数: 21 | 22 | $$ 23 | \begin{split} 24 | P(x) &= \lambda\,e^{-\lambda x} \\\\ 25 | F(x) &= \int\_0^x P(x)\,dx = 1 - e^{-\lambda x} 26 | \end{split} 27 | $$ 28 | 29 | 假设我们用n个延时值的最大值作为估计,即有$$$ X = Max(X\_1, X\_2, ..., X\_n) $$$,可得X的累积分布函数: 30 | 31 | $$ 32 | \begin{split} 33 | F\_X(x) &= P(X < x) \\\\ 34 | &= P(Max(X\_1, X\_2, \cdots, X\_n) < x) \\\\ 35 | &= P(X\_1 < x)\,P(X\_2 < x) \cdots P(X\_n < x) \\\\ 36 | &= \left( F(x) \right)^n \\\\ 37 | &= \left( 1 - e^{-\lambda x} \right)^n 38 | \end{split} 39 | $$ 40 | 41 | 从X的累积分布函数我们可以直接得出X的期望E(X) (具体推导有点复杂,这里不详述,可以[参考这里](https://www.stat.berkeley.edu/~mlugo/stat134-f11/exponential-maximum.pdf)): 42 | 43 | $$ 44 | \begin{split} 45 | E(X) &= \int\_0^\infty 1 - F\_X(x)\;dx \\\\ 46 | &= \int\_0^\infty 1 - \left( 1 - e^{-\lambda x} \right)^n \;dx \\\\ 47 | &= \frac 1\lambda \,\left( 1 + \frac 1 2 + \frac 1 3 + \cdots + \frac 1 n \right) \\\\ 48 | &= \frac 1\lambda \sum\_{i=1}^n \frac 1 i \\\\ 49 | \end{split} 50 | $$ 51 | 52 | 53 | E(X)是我们的估计值,我们估计的目标是什么来?是perceltile值(如99.9%分位对应的延时值,分位数下面用p表示),它的值为: 54 | 55 | $$ 56 | \begin{split} 57 | F^{-1}(p) &= \frac {-ln(1-p)} \lambda \\\\ 58 | \end{split} 59 | $$ 60 | 61 | 使 $$$ E(X) = F^{-1}(p) $$$,我们可以求解n值,实际上,这个等式可以简化下: 62 | 63 | $$ 64 | \begin{split} 65 | E(X) &= F^{-1}(p) \\\\ 66 | \Rightarrow \sum\_{i=1}^n \frac 1 i &= -ln(1-p) \\\\ 67 | \end{split} 68 | $$ 69 | 70 | 从以上公式可看出,给定percentile的百分位p值,我们可以很容易使用程序计算出n值。也即当取这个n值时,我们使用最大值的期望(平均值)可以无偏地估计percentile值。到这里可能有人会问,均值没问题了,方差怎么办,会有大的误差吗?其实无需担心,方差的问题,我们默认已经通过[中心极限定理](https://en.wikipedia.org/wiki/Central_limit_theorem)来解决了:我们不会直接使用max值来做最后的估计,而是不断地算多个max值后求平均,根据中心极限定理最终平均值的方差会随着样本量的大量增加而大大降低。 71 | 72 | 除了数学推导,我们还可以从下面这张通过程序模拟的概率分布图来更形象地理解估计过程: 73 | 74 | ![max_pdf_vs_orig_cdf](/res/201605-percentile_probability_estimate/max_pdf_vs_orig_cdf.png) 75 | 76 | 黑色曲线(左侧y轴)是待统计指标的积累分布,我们的目标是想统计左侧y轴等于一定百分位时,对应的x轴的值。红色是我们使用最大值做为估计量的概率密度分布,可以看出,它的期望值(均值)即对应于x轴上我们的估计目标值。 77 | 78 | 以上我们是以指数分布对模型进行分析,实际测试对正态分布也用同样的估计方法,误差仍然非常小,所以此方法对真实场景还是比较可靠的。 79 | 80 | ## 程序实现 81 | 82 | 程序实现上是格外简单的,我们以python为例,首先看一下根据百分位计算参数n的值的实现: 83 | 84 | ```python 85 | import sys 86 | import math 87 | import string 88 | 89 | def get_n_for_percentile(percentile): 90 | n = 1 91 | expected_value = 0 92 | percentile_value = -math.log(1 - string.atof(percentile)/100) 93 | while expected_value < percentile_value: 94 | expected_value = expected_value + 1.0/n 95 | n = n + 1 96 | return n 97 | 98 | if __name__ == '__main__': 99 | print get_n_for_percentile(sys.argv[1]) 100 | ``` 101 | 102 | 可以像下面这样运行它: 103 | 104 | $ python n_for_percentile.py 99 105 | 57 106 | $ python n_for_percentile.py 99.9 107 | 562 108 | 109 | 从运行结果我们可看出,如果要估计99%分位值,我们需要n=57,即统计57个样本的最大值,如果要估计99.9%,则需要n=562。 110 | 111 | 下面再看一下实际统计的代码,也十分简单,计算上它只需要做比较赋值等运算,空间上则仅需要3个状态变量: 112 | 113 | ```python 114 | import sys 115 | import random 116 | import n_for_percentile 117 | 118 | n = n_for_percentile.get_n_for_percentile('99.9') 119 | 120 | # only three status variables needed 121 | counter = 0 122 | max_of_n = 0 123 | sum_of_max = 0 124 | 125 | def percentile_estimate_proc(latency): 126 | global n, counter, max_of_n, sum_of_max 127 | if latency > max_of_n: 128 | max_of_n = latency 129 | counter = counter + 1 130 | if counter % n == 0: 131 | sum_of_max = sum_of_max + max_of_n 132 | max_of_n = 0 133 | 134 | def get_cur_percentile_estimate(): 135 | global n, counter, sum_of_max 136 | return sum_of_max / (counter / n) 137 | 138 | if __name__ == '__main__': 139 | for i in range(0, 100000): 140 | # use uniform distribution here just for simplicity 141 | percentile_estimate_proc(random.uniform(0, 1000)) 142 | print get_cur_percentile_estimate() 143 | ``` 144 | 145 | 另外,此算法也能很容易适用于集群汇总统计,即合并多台服务器的统计值,实际只需将上述代码中counter与sum\_of\_max进行汇总合并即可。 146 | 147 | ## 总结 148 | 149 | 本文给出一种基于概率估计的计算percentile值的方法,时间和空间复杂度都极低,十分适合在对精度要求不是太高,又需要尽量高效和实现简单的业务场景。 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /2016-06-05-use_gtestx_for_benchmark.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 使用gtestx写C++性能测试 3 | date: 2016-06-05 01:00:00 +0800 4 | categories: [programming] 5 | tags: [C++] 6 | math: false 7 | --- 8 | 在互联网后台开发领域,针对性能的单元测试常被忽视,实际上它非常重要,至少与普通的unit-test一样重要。特别是对一些基础库、关键部分代码,必要的性能单元测试,可帮助在模块粒度上发现并解决性能问题,这比在系统粒度上分析解决性能问题要容易地多。更为重要的,关注并熟悉模块级性能,是架构师掌控系统整体性能的基础,这在架构设计、线上问题分析等场景中会经常被使用。在过去几年工作中我写过很多的性能单测代码,它们帮助我更好地理解各种系统调用、数据结构、组件的性能;在我经历的几个海量系统研发过程中,严格的性能单测也对项目最终质量起到了显著的作用。 9 | 10 | 我在写性能测试方面(主要是C/C++)还算是有过不少经验,写这种代码虽不难,但是比较重复和枯燥,后来我尝试写了一些框架工具来简化这类工作,gtestx就是其中之一,开始只是个人工具,最近一年多在项目团队中推广使用大家觉得还不错。今天就系统性地介绍下这个工具。 11 | 12 | gtestx是基于gtest的一个扩展,从取名上也很容易理解。为什么基于gtest呢?因为首先gtest基本上是现在C++的单元测试的主流选择,并且有广泛的群众基础,基于gtest来做测试测试可以更容易上手,也就是说,如果你已经在用gtest写单元测试了,基本不需要学习和额外工作就可以直接写gtestx的tests;另外gtest已经实现了一套比较先进好用的tests管理机制,性能单测本质上说也是一种单元测试,所以gtest很多优秀的特性gtestx可以直接继承复用。 13 | 14 | 下面我们就从几个实例出发,来介绍一下gtestx的用法。 15 | 16 | ## 简单用例 17 | 18 | 首先一个简单的例子,测试系统调用getppid的性能: 19 | 20 | ```C++ 21 | #include 22 | #include 23 | #include "gtestx/gtestx.h" 24 | 25 | // Just like TEST macro, use PERF_TEST for performance testing 26 | PERF_TEST(SimpleTest, SysCallPerf) 27 | { 28 | getppid(); 29 | } 30 | ``` 31 | 32 | 可以看出它跟gtest的TEST宏用法很像: 33 | 34 | ```C++ 35 | TEST(SimpleTest, SysCall) 36 | { 37 | ASSERT_NE(0, getppid()); 38 | } 39 | ``` 40 | 41 | 实际上PERF\_TEST跟TEST确实非常类似,它的两个参数的含意与TEST相同,都是表示gtest框架中的TestCaseName和TestName。所不同的是,PERF\_TEST的函数体并非只像TESt的函数体一样只运行一次,而是(默认)会死循环一段特定的时间并统计相关counter。在用例执行时,PERF\_TEST也会自动将统计的性能数据打印出来: 42 | 43 | ``` 44 | $ ./gtestx_examples --gtest_filter='SimpleTest.*' 45 | [==========] Running 2 tests from 1 test case. 46 | [----------] Global test environment set-up. 47 | [----------] 2 tests from SimpleTest 48 | [ RUN ] SimpleTest.SysCall 49 | [ OK ] SimpleTest.SysCall (0 ms) 50 | [ RUN ] SimpleTest.SysCallPerf 51 | count: 25,369,636 52 | time: 1.508939 s 53 | HZ: 16,812,897.009090 54 | 1/HZ: 0.000000059 s 55 | [ OK ] SimpleTest.SysCallPerf (1509 ms) 56 | [----------] 2 tests from SimpleTest (1509 ms total) 57 | 58 | [----------] Global test environment tear-down 59 | [==========] 2 tests from 1 test case ran. (1510 ms total) 60 | [ PASSED ] 2 tests. 61 | ``` 62 | 63 | 从上面可以看出SimpleTest.SysCallPerf这个性能测试Test一共循环执行了25369636次,总共1.5秒时长,调用性能为每秒1681万次,平均每次调用耗时59纳秒。 64 | 65 | ## Fixture用例 66 | 67 | 很多时候我们性能测试的过程是,先初始化需要的状态,然后性能测试,最后清理资源。这时我们适合使用gtest中的Fixture用例的gtestx版本。我们举一个具体一点的例子,比如要测试std::map的查找性能,需要先在map中插入一定数量的元素,然后执行性能单测,可以实现为下面的代码: 68 | 69 | ```C++ 70 | #include 71 | #include "gtestx/gtestx.h" 72 | 73 | class StdMapTest : public testing::Test 74 | { 75 | protected: 76 | virtual ~StdMapTest() {} 77 | virtual void SetUp() override { 78 | for (int i = 0; i < 1000; i++) { 79 | map_.emplace(i, 1); 80 | } 81 | } 82 | virtual void TearDown() override {} 83 | 84 | std::map map_; 85 | }; 86 | 87 | PERF_TEST_F(StdMapTest, FindPerf) 88 | { 89 | map_.find(999); 90 | } 91 | ``` 92 | 93 | 如上代码中,初始化部分在SetUp函数中完成,PERF\_TEST\_F宏是原有TEST\_F宏的一个gtestx版本,它只需一行代码,非常简单。用例执行结果如下: 94 | 95 | ``` 96 | $ ./gtestx_examples --gtest_filter='StdMapTest.FindPerf' 97 | [==========] Running 1 test from 1 test case. 98 | [----------] Global test environment set-up. 99 | [----------] 1 test from StdMapTest 100 | [ RUN ] StdMapTest.FindPerf 101 | count: 3,187,592 102 | time: 1.508904 s 103 | HZ: 2,112,521.406266 104 | 1/HZ: 0.000000473 s 105 | [ OK ] StdMapTest.FindPerf (1512 ms) 106 | [----------] 1 test from StdMapTest (1512 ms total) 107 | 108 | [----------] Global test environment tear-down 109 | [==========] 1 test from 1 test case ran. (1512 ms total) 110 | [ PASSED ] 1 test. 111 | ``` 112 | 113 | 114 | ## 带参数的Fixture用例 115 | 116 | 假设我们要测试std::unordered\_map的查找性能,但hash表的性能跟它的桶数量有很大的关系,我们希望测试在不同节点数/桶数量的配置下的查找性能的情况。若用普通方法我们需要根据不同的配置写多个用例,但使用带参数的Fixture用例一个就可以搞定了: 117 | 118 | ```C++ 119 | #include 120 | #include "gtestx/gtestx.h" 121 | 122 | class HashMapTest : public testing::TestWithParam 123 | { 124 | protected: 125 | virtual ~HashMapTest() {} 126 | virtual void SetUp() override { 127 | map_.max_load_factor(GetParam()); 128 | for (int i = 0; i < 1000; i++) { 129 | map_.emplace(i, 1); 130 | } 131 | } 132 | virtual void TearDown() override {} 133 | 134 | std::unordered_map map_; 135 | int search_key_ = 0; 136 | }; 137 | 138 | INSTANTIATE_TEST_CASE_P(LoadFactor, HashMapTest, 139 | testing::Values(3.0, 1.0, 0.3)); 140 | 141 | PERF_TEST_P(HashMapTest, FindPerf) 142 | { 143 | map_.find(++search_key_); 144 | } 145 | ``` 146 | 147 | 上面代码中我们使用INSTANTIATE\_TEST\_CASE\_P宏设定需要测试的不同参数值(这点与gtest中是完全相同),然后使用PERF\_TEST\_P定义性能测试的代码(跟gtest中的TEST\_P宏对应)。它的执行结果如下,可以看出性能随着节点/桶数的比例参数下降而升高: 148 | 149 | ``` 150 | $ ./gtestx_examples --gtest_filter='*HashMapTest.FindPerf*' 151 | [==========] Running 3 tests from 1 test case. 152 | [----------] Global test environment set-up. 153 | [----------] 3 tests from LoadFactor/HashMapTest 154 | [ RUN ] LoadFactor/HashMapTest.FindPerf/0 155 | count: 4,629,181 156 | time: 1.509068 s 157 | HZ: 3,067,576.146337 158 | 1/HZ: 0.000000326 s 159 | [ OK ] LoadFactor/HashMapTest.FindPerf/0 (1511 ms) 160 | [ RUN ] LoadFactor/HashMapTest.FindPerf/1 161 | count: 11,975,484 162 | time: 1.508859 s 163 | HZ: 7,936,781.369233 164 | 1/HZ: 0.000000126 s 165 | [ OK ] LoadFactor/HashMapTest.FindPerf/1 (1510 ms) 166 | [ RUN ] LoadFactor/HashMapTest.FindPerf/2 167 | count: 16,320,267 168 | time: 1.508913 s 169 | HZ: 10,815,909.863591 170 | 1/HZ: 0.000000092 s 171 | [ OK ] LoadFactor/HashMapTest.FindPerf/2 (1509 ms) 172 | [----------] 3 tests from LoadFactor/HashMapTest (4530 ms total) 173 | 174 | [----------] Global test environment tear-down 175 | [==========] 3 tests from 1 test case ran. (4530 ms total) 176 | [ PASSED ] 3 tests. 177 | ``` 178 | 179 | ## 模板Fixture用例 180 | 181 | 又假设我们要测试std::queue的进队出队的性能,并且需要考虑不同的元素类型(std::queue的模板参数类型)下的性能情况,我们可以使用基于模板Fixture的用例: 182 | 183 | ```C++ 184 | #include 185 | #include "gtestx/gtestx.h" 186 | 187 | template 188 | class QueueTest : public testing::Test 189 | { 190 | protected: 191 | virtual ~QueueTest() {} 192 | virtual void SetUp() override {} 193 | virtual void TearDown() override {} 194 | 195 | std::queue queue_; 196 | }; 197 | 198 | typedef testing::Types> TestTypes; 199 | TYPED_TEST_CASE(QueueTest, TestTypes); 200 | 201 | TYPED_PERF_TEST(QueueTest, PushPopPerf) 202 | { 203 | this->queue_.push(TypeParam()); 204 | this->queue_.pop(); 205 | } 206 | ``` 207 | 208 | 如上面代码,我们使用TYPED\_TEST\_CASE定义了int和std::vector两个类参数分别来做性能测试,这时基于模板的用例使用TYPED\_PERF\_TEST定义(与gtest中的TYPED\_TEST对应)。执行结果如下: 209 | 210 | ``` 211 | $ ./gtestx_examples --gtest_filter='QueueTest*PushPopPerf*' 212 | [==========] Running 2 tests from 2 test cases. 213 | [----------] Global test environment set-up. 214 | [----------] 1 test from QueueTest/0, where TypeParam = int 215 | [ RUN ] QueueTest/0.PushPopPerf 216 | count: 37,617,883 217 | time: 1.509303 s 218 | HZ: 24,924,009.956914 219 | 1/HZ: 0.000000040 s 220 | [ OK ] QueueTest/0.PushPopPerf (1509 ms) 221 | [----------] 1 test from QueueTest/0 (1509 ms total) 222 | 223 | [----------] 1 test from QueueTest/1, where TypeParam = std::vector > 224 | [ RUN ] QueueTest/1.PushPopPerf 225 | count: 9,433,683 226 | time: 1.508871 s 227 | HZ: 6,252,146.803802 228 | 1/HZ: 0.000000160 s 229 | [ OK ] QueueTest/1.PushPopPerf (1509 ms) 230 | [----------] 1 test from QueueTest/1 (1509 ms total) 231 | 232 | [----------] Global test environment tear-down 233 | [==========] 2 tests from 2 test cases ran. (3018 ms total) 234 | [ PASSED ] 2 tests. 235 | ``` 236 | 237 | ## PERF\_ABORT 238 | 239 | 如果你想在PERF函数体中使用ASSERT\_XXX等gtest宏,需要注意当断言fail时默认它只能跳出当前函数还不是整个性能测试的用例,所以gtestx中增加了一个辅助宏来帮助能正确地跳出整个用例。所以当使用ASSERT\_XXX等宏时,应当使用类似下面的代码: 240 | 241 | ```C++ 242 | PERF_TEST(MiscFeaturesTest, PerfAbort) 243 | { 244 | ASSERT_NE(0, getppid()) << PERF_ABORT; 245 | } 246 | ``` 247 | 248 | ## 指定频率和时长 249 | 250 | 默认gtestx对每个PERF用例的执行频率总是死循环压满,执行时长默认是固定的(1.5秒)。这两个参数实际都可以修改。什么场景需要修改呢?比如我们希望观察一定调用频率下的CPU或其它资源消耗,可以修改执行频率参数为某一固定值;再比如我们希望执行更长时间以观察性能的平均情况,则可以修改执行时长的参数。如何修改?gtestx考虑灵活性提供了2种方式。 251 | 252 | 方式一,使用带\_OPT后缀的参数扩展宏(实际所有的gtestx宏都有一个\_OPT版本),这种方式适用于某个特别的用例固定地需要一个特殊的频率或时长配置的场景。取一个例子说明: 253 | 254 | ```C++ 255 | PERF_TEST_OPT(MiscFeaturesTest, SysCallPerf, 10000, 5000) 256 | { 257 | getppid(); 258 | } 259 | ``` 260 | 261 | 以上代码以10000 calls/s的频率执行,同时一共执行5000 ms。如果频率和时长其中某个参数不想修改,只需填入-1(表示默认值)。 262 | 263 | 方式二,通过命令行参数修改(这种方式可以override方式一的修改),只需在命令行上使用-hz及-time两个选项来分别指定执行频率和时长参数。这种方式适用于在执行时临时修改不同的参数以观测比较执行结果的场景。以下为一例: 264 | 265 | ``` 266 | ./gtestx_examples --gtest_filters='SimpleTest.*' -hz=10000 -time=10000 267 | ``` 268 | 269 | ## 实现和代码 270 | 271 | gtestx的实现原理并不复杂,它其实是hack了gtest的用例宏,在用例层之上又包装一层性能测试逻辑。 272 | 273 | 如有兴趣,你可以在[这里](https://github.com/mikewei/gtestx)获取代码,其中也包括一些文档和示例代码。 274 | 275 | ## 小结 276 | 277 | 本文简单介绍了gtestx以及其常用的做性能单元测试的方法,希望对你的工作有所帮助。 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /2017-02-04-shm_container_intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: shm_container发布1.0.0-beta 3 | date: 2017-02-04 22:00:00 +0800 4 | categories: [open-source] 5 | tags: [shm, C++] 6 | math: false 7 | --- 8 | ## 背景 - 共享内存模式 9 | 10 | 要理解这个组件的作用,得从共享内存(SHM)说起。共享内存通常被理解为一种IPC(进程间通信)机制,实际上它的意义远多于此。我之前所工作过的鹅厂团队或许是最早把它玩转,并作为所有服务器软件的开发基石来使用的,可以说围绕其已有一套较完整的设计模式和方法论。这里简单列举一下这套模式的过人之处: 11 | 12 | * 模块化与解耦,鹅厂有个方法论叫大系统小做,用现在流行词叫微服务,其实将近20年前就已经有团队用SHM来实践了 13 | * 风险隔离与高可用,也是上一点的结果,对于C/C++服务特别有用,如果你经历过批量SEGFAULT会深有感触 14 | * 进程重启仍能保留状态,这个在有状态服务场景非常有用,玩法很多 15 | * 可运营性强,基本所有内存资源预分配,用量方便监控 16 | * 极高性能,无锁无IO,比RPC方式高几个量级 17 | 18 | 客观地说,共享内存模式在十多年前的优势和必要性更大一些,因为那个时代没有RPC、没有kafka、没有Redis,性能成本更敏感。但即使今天,这套技术在高性能、高可靠的需求场景下,仍有很大的用武之地。这些年我见过不少C/C++项目,大多是线程、指针满天飞,雪崩、crash、泄漏常在,很多优化空间。 19 | 20 | 为什么对于共享内存,大家特别是小团队用得并不多呢,我想一个是缺少经验,另一个更重要的原因是,门槛确实很高。首先操作系统提供多套API,SysV-IPC、POSIX、hugeTLB选项、各种参数和坑。然后是你得自己管理内存,这比使用STL困难多了,依然有很多坑等着你。即使有一些封装库,真正能做到简单易用的也很困难。 21 | 22 | 这也是我写这个基础库shm_container的初衷,因自身有多年SHM编程经验,加上现在C++11等工具的成熟,希望重新设计一个C++库,可以更高效和可靠地使用共享内存,接近像使用STL一样容易地使用它。 23 | 24 | ## 设计 - 小而美且壮 25 | 26 | 下面简单介绍下这个库的架构,如下图: 27 | 28 | ![](/res/201702-shm_container_intro/shm_container_arch.png) 29 | 30 | 基本分成两层,上面一层是容器层,提供了使用共享内存进行存储、传递数据的高级接口;底下一层是共享内存分配器的抽象和实现层,可以让不同的容器方便地选择使用任何一种共享内存机制。分别具体一下,先看容器: 31 | 32 | 33 | | Container | Description | 34 | |-------------------|---------------------------------------------------| 35 | | ShmArray | 存储固定大小的结构体或结构体数组 | 36 | | ShmQueue | 单写单读无锁FIFO队列,支持ZeroCopy方式读写 | 37 | | ShmSyncBuf | 单写多读的无锁广播队列,多用于数据分发或同步 | 38 | | ShmHashTable | 高性能固定结点大小多阶Hash表,这里有[算法介绍][ht] | 39 | | ShmLinkTable | 基于块链表的变长内存分配器 | 40 | | ShmHashMap | 值可变长的HashMap,它基于ShmHashTable和ShmLinkTable | 41 | 42 | 共享内存分配器有: 43 | 44 | 45 | | Allocator | Description | 46 | |-------------------|---------------------------------------------------| 47 | | SVIPC | 基于最传统的SysV-IPC的共享内存分配器 | 48 | | SVIPC_HugeTLB | 使用HugeTLB大页的共享内存分配器,需通过SysV-IPC接口 | 49 | | POSIX | 基于POSIX共享内存接口的分配器,一大优点是字符串名字 | 50 | | ANON | 基于共享匿名页的分配器,可以父子进程间数据共享 | 51 | | HEAP | 基于进程私有内存的分配器(并不是共享内存了) | 52 | 53 | 还有的一些优点: 54 | 55 | * 精心设计的接口,简单程度接近于STL 56 | * 充分的防御性编程代码和异常检查代码 57 | * 充分的单元测试,目前约有200+单测用例 58 | * 小巧,基础库代码全.h,且无任何第三方依赖 59 | * 充分的文档注释,也提供在线文档 60 | * 代码完全符合google C++ code style 61 | 62 | ## 性能 - 目标极致 63 | 64 | 从设计到实现,高性能始终会作为主要的考虑。所有实现均为无锁实现。对基础库的大部分接口,都会有[gtestx](https://github.com/mikewei/gtestx)性能单测。直接贴下数据: 65 | 66 | | Container | Operation | OPS | 67 | |-------------------|--------------------|----------------| 68 | | ShmQueue | 写入 | 1200万/s | 69 | | ShmSyncBuf | 读出 | 1500万/s | 70 | | ShmHashTable | 查询 | 1500万/s | 71 | | ShmLinkTable | 读取 | 2000万/s | 72 | | ShmHashMap | 查询 | 500万/s (*) | 73 | 74 | (*) 此接口目前仅提供了std::string版,性能部分受限于动态内存分配 75 | 76 | 77 | ## 资源 78 | 79 | 最新代码可在github上获得:[https://github.com/mikewei/shm_container](https://github.com/mikewei/shm_container) 80 | 81 | 在线文档可在此查看: [SHM-Container Documentation](https://mikewei.github.io/doc/shm_container) 82 | 83 | 84 | [ht]: https://github.com/mikewei/blogs/blob/master/201512-high_performance_hash_table.md 85 | -------------------------------------------------------------------------------- /res/201512-high_performance_hash_table/array_usage_vs_rand_writes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201512-high_performance_hash_table/array_usage_vs_rand_writes.png -------------------------------------------------------------------------------- /res/201512-high_performance_hash_table/effective_rows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201512-high_performance_hash_table/effective_rows.png -------------------------------------------------------------------------------- /res/201512-high_performance_hash_table/fix_ht_usage_vs_rows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201512-high_performance_hash_table/fix_ht_usage_vs_rows.png -------------------------------------------------------------------------------- /res/201512-high_performance_hash_table/ht_250w_ratio60_rows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201512-high_performance_hash_table/ht_250w_ratio60_rows.png -------------------------------------------------------------------------------- /res/201512-high_performance_hash_table/ht_usage_depth_vs_ratio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201512-high_performance_hash_table/ht_usage_depth_vs_ratio.png -------------------------------------------------------------------------------- /res/201605-hashtable_with_2hash/linked_ht_1vs2_avg_depth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-hashtable_with_2hash/linked_ht_1vs2_avg_depth.png -------------------------------------------------------------------------------- /res/201605-hashtable_with_2hash/linked_ht_1vs2_max_depth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-hashtable_with_2hash/linked_ht_1vs2_max_depth.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_irqsched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_irqsched.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_irqsched2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_irqsched2.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_multiqueue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_multiqueue.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_overview.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_reuseport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_reuseport.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_reuseport2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_reuseport2.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_rfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_rfs.png -------------------------------------------------------------------------------- /res/201605-linux_net_parallel/linux_net_rps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-linux_net_parallel/linux_net_rps.png -------------------------------------------------------------------------------- /res/201605-percentile_probability_estimate/max_pdf_vs_orig_cdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201605-percentile_probability_estimate/max_pdf_vs_orig_cdf.png -------------------------------------------------------------------------------- /res/201702-shm_container_intro/shm_container_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikewei/blogs/bafc2b704c528e57df964a6c0b31e93e861f6a22/res/201702-shm_container_intro/shm_container_arch.png --------------------------------------------------------------------------------