Libev中在管理定时器时,使用了堆这种结构,而且除了常见的最小2叉堆之外,它还实现了更高效的4叉堆。

之所以要实现4叉堆,是因为普通2叉堆的缓存效率较低,所谓缓存效率低,也就是说对CPU缓存的利用率比较低,说白了,就是违背了局部性原理。这是因为在2叉堆中,对元素的操作通常在N和N/2之间进行,所以对于含有大量元素的堆来说,两个操作数之间间隔比较远,对CPU缓存利用不太好。Libev中的注释说明,对于元素个数为50000+的堆来说,4叉堆的效率要提高5%所有。

在看Libev中堆的实现代码之前,先来看一个基本定理:对于n叉堆来说,使用数组进行存储时,下标为x的元素,其孩子节点的下标范围是[nx+1, nx+n]。比如2叉堆,下标为x的元素,其孩子节点的下标为2x+1和2x+2.

根据定理,对于4叉堆而言,下标为x的元素,其孩子节点的下标范围是[4x+1, 4x+4]。还可以得出,其父节点的下标是(x-1)/4。然而在Libev的代码中,使用数组a存储堆时,4叉堆的第一个元素存放在a[3],2叉堆的第一个元素存放在a[1]。

所以,对于Libev中的4叉堆实现而言,下标为k的元素(对应在正常实现中的下标是k-3),其孩子节点的下标范围是[4(k-3)+1+3, 4(k-3)+4+3];其父节点的下标是((k-3-1)/4)+3。

对于Libev中的2叉堆实现而言,下标为k的元素(对应在正常实现中,其下标是k-1),其孩子节点的下标范围是[2(k-1)+1+1,  2(k-1)+2+1],也就是[2k, 2k+1];其父节点的下标是((k-1-1)/2)+1,也就是k/2。

下面来看Libev中的代码:

1:堆元素

#if EV_HEAP_CACHE_AT
/* a heap element */
typedef struct {
ev_tstamp at;
WT w;
} ANHE; #define ANHE_w(he) (he).w /* access watcher, read-write */
#define ANHE_at(he) (he).at /* access cached at, read-only */
#define ANHE_at_cache(he) (he).at = (he).w->at /* update at from watcher */
#else
/* a heap element */
typedef WT ANHE; #define ANHE_w(he) (he)
#define ANHE_at(he) (he)->at
#define ANHE_at_cache(he)
#endif

ANHE就是堆元素,它要么就是一个指向时间监视器结构ev_watcher_time的指针(WT),要么除了包含该指针之外,还缓存了ev_watcher_time中的成员at。堆中元素就是根据at的值进行组织的,具有最小at值得节点就是根节点。

在Libev中,为了提高缓存命中率,在堆中缓存了元素at,文档中的原文是:

Heaps are not very cache-efficient. To improve the cache-efficiency of the timer and periodics heaps, libev can cache the timestamp (at) within the heap structure(selected by defining
EV_HEAP_CACHE_AT to 1), which uses 8-12 bytes more per watcher and a few hundred bytes more code, but avoids random read accesses on heap changes. This improves performance noticeably with many (hundreds) ofwatchers.

2:宏定义

#if EV_USE_4HEAP

#define DHEAP 4
#define HEAP0 (DHEAP - 1) /* index of first element in heap */
#define HPARENT(k) ((((k) - HEAP0 - 1) / DHEAP) + HEAP0)
#define UPHEAP_DONE(p,k) ((p) == (k))
...
#else #define HEAP0 1
#define HPARENT(k) ((k) >> 1)
#define UPHEAP_DONE(p,k) (!(p))
...

其中的宏HEAP0表示堆中第一个元素的下标;HPARENT是求下标为k的节点的父节点下标;UPHEAP_DONE宏用于向上调整堆时,判断是否已经到达了根节点,对于4叉堆而言,根节点下标为3,其父节点的下标根据公式得出,也是3,所以结束的条件((p) == (k)),对于2叉堆而言,根节点下标为1,其父节点根据公式得出下标为0,所以结束的条件是(!(p))

3:向下调整堆

首先是4叉堆:

void downheap (ANHE *heap, int N, int k)
{
ANHE he = heap [k];
ANHE *E = heap + N + HEAP0; for (;;)
{
ev_tstamp minat;
ANHE *minpos;
ANHE *pos = heap + DHEAP * (k - HEAP0) + HEAP0 + 1; /* find minimum child */
if (expect_true (pos + DHEAP - 1 < E))
{
/* fast path */
(minpos = pos + 0), (minat = ANHE_at (*minpos));
if (ANHE_at (pos [1]) < minat)
(minpos = pos + 1), (minat = ANHE_at (*minpos));
if (ANHE_at (pos [2]) < minat)
(minpos = pos + 2), (minat = ANHE_at (*minpos));
if (ANHE_at (pos [3]) < minat)
(minpos = pos + 3), (minat = ANHE_at (*minpos));
}
else if (pos < E)
{
/* slow path */
(minpos = pos + 0), (minat = ANHE_at (*minpos));
if (pos + 1 < E && ANHE_at (pos [1]) < minat)
(minpos = pos + 1), (minat = ANHE_at (*minpos));
if (pos + 2 < E && ANHE_at (pos [2]) < minat)
(minpos = pos + 2), (minat = ANHE_at (*minpos));
if (pos + 3 < E && ANHE_at (pos [3]) < minat)
(minpos = pos + 3), (minat = ANHE_at (*minpos));
}
else
break; if (ANHE_at (he) <= minat)
break; heap [k] = *minpos;
ev_active (ANHE_w (*minpos)) = k; k = minpos - heap;
} heap [k] = he;
ev_active (ANHE_w (he)) = k;
}

如果理解普通二叉堆的向下调整算法的话,上面的代码还是很容易理解的。参数heap表示堆的起始地址,N表示堆中实际元素的总数,k表示需要调整元素的下标。

E表示堆中最后一个元素的下一个元素,用于判断是否已经到达了末尾。在foo循环中,首先得到节点heap [k]的第一个子节点的指针pos,pos + DHEAP – 1表示最后一个子节点的指针。

依次比较4个子节点,找到heap[k]所有子节点中的最小元素minpos。如果heap [k]的at值比minpos的at值还小,说明已经符合堆结构了,直接退出循环即可。否则的话,将minpos上移,依次循环下去。

ev_active(ANHE_w (*minpos)) = k,将时间监视器的active成员置为其在堆中的下标。

然后是2叉堆:

void downheap (ANHE *heap, int N, int k)
{
ANHE he = heap [k]; for (;;)
{
int c = k << 1; if (c >= N + HEAP0)
break; c += c + 1 < N + HEAP0 && ANHE_at (heap [c]) > ANHE_at (heap [c + 1]) ? 1 : 0; if (ANHE_at (he) <= ANHE_at (heap [c]))
break; heap [k] = heap [c];
ev_active (ANHE_w (heap [k])) = k; k = c;
} heap [k] = he;
ev_active (ANHE_w (he)) = k;
}

2叉堆的实现原理与4叉堆一样,不再赘述。

4:向上调整堆

void upheap (ANHE *heap, int k)
{
ANHE he = heap [k]; for (;;)
{
int p = HPARENT (k); if (UPHEAP_DONE (p, k) || ANHE_at (heap [p]) <= ANHE_at (he))
break; heap [k] = heap [p];
ev_active (ANHE_w (heap [k])) = k;
k = p;
} heap [k] = he;
ev_active (ANHE_w (he)) = k;
}

代码较简单,要调整的节点下标为k,首先得到其父节点下标p,然后判断heap[k]和heap[p]的关系作出调整。

5:其余代码

void adjustheap (ANHE *heap, int N, int k)
{
if (k > HEAP0 && ANHE_at (heap [k]) <= ANHE_at (heap [HPARENT (k)]))
upheap (heap, k);
else
downheap (heap, N, k);
} /* rebuild the heap: this function is used only once and executed rarely */
void reheap (ANHE *heap, int N)
{
int i; /* we don't use floyds algorithm, upheap is simpler and is more cache-efficient */
/* also, this is easy to implement and correct for both 2-heaps and 4-heaps */
for (i = 0; i < N; ++i)
upheap (heap, i + HEAP0);
}

Libev源码分析03:Libev使用堆管理定时器的更多相关文章

  1. [转]Libev源码分析 -- 整体设计

    Libev源码分析 -- 整体设计 libev是Marc Lehmann用C写的高性能事件循环库.通过libev,可以灵活地把各种事件组织管理起来,如:时钟.io.信号等.libev在业界内也是广受好 ...

  2. NIO 源码分析(03) 从 BIO 到 NIO

    目录 一.NIO 三大组件 Channels.Buffers.Selectors 1.1 Channel 和 Buffer 1.2 Selector 1.3 Linux IO 和 NIO 编程的区别 ...

  3. 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 百篇博客分析OpenHarmony源码 | v63.01

    百篇博客系列篇.本篇为: v63.xx 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...

  4. 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 百篇博客分析OpenHarmonyOS | v2.07

    百篇博客系列篇.本篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核 ...

  5. Libev源码分析09:select突破处理描述符个数的限制

    众所周知,Linux下的多路复用函数select采用描述符集表示处理的描述符.描述符集的大小就是它所能处理的最大描述符限制.通常情况下该值为1024,等同于每个进程所能打开的描述符个数. 增大描述符集 ...

  6. JDK1.8源码分析03之idea搭建源码阅读环境

    序言:上一节说了阅读源码的顺序,有了一个大体的方向,咱们就知道该如何下手.接下来,就要搭建一个方便阅读源码及debug的环境.有助于跟踪源码的调用情况. 目前新开发的项目, 大多数都是基于JDK1.8 ...

  7. Spark源码分析之九:内存管理模型

    Spark是现在很流行的一个基于内存的分布式计算框架,既然是基于内存,那么自然而然的,内存的管理就是Spark存储管理的重中之重了.那么,Spark究竟采用什么样的内存管理模型呢?本文就为大家揭开Sp ...

  8. Springboot源码分析之事务拦截和管理

    摘要: 在springboot的自动装配事务里面,InfrastructureAdvisorAutoProxyCreator ,TransactionInterceptor,PlatformTrans ...

  9. Libev源码分析05:Libev中的绝对时间定时器

    Libev中的超时监视器ev_periodic,是绝对时间定时器,不同于ev_timer,它是基于日历时间的.比如如果指定一个ev_periodic在10秒之后触发(ev_now() + 10),然后 ...

随机推荐

  1. IO流10 --- 缓冲流(字节型)实现非文本文件的复制 --- 技术搬运工(尚硅谷)

    字节型缓冲流,BufferedOutputStream默认缓冲区大小 8192字节byte,满了自动flush() @Test public void test6(){ File srcFile = ...

  2. oracle -视图 序列 约束

    1.视图 视图是基于一个或者多个表数据库对象,视图允许用户创建一个无数据的”伪表“,视图只是一个获取特定列好行的sql查询组成,通过视图检索数据就像从表中检索数据 一样. 视图可以提供一个附加的安全层 ...

  3. JSP Web第五章整理复习 JSP访问数据库

    P164  例5-1  常用SQL语句 P178  数据库连接池 (1)连接池的作用 存储多个数据库连接对象,当程序需要时,从池中获取1个连接,程序执行完成后再还给连接池.避免数据库连接建立.关闭的开 ...

  4. 50倍时空算力提升,阿里云RDS PostgreSQL GPU版本上线

    2019年3月19日,阿里云RDS PostgreSQL数据库GPU规格版本正式上线,开启了RDS异构计算并行加速之路.该版本在RDS(关系型数据库服务)的云基础设施层面首次完成了与阿里云异构计算产品 ...

  5. CodePlus2017 12月月赛 div2火锅盛宴

    当时看到这道题感觉真是难过,我数据结构太弱啦. 我们来看看需要求什么: 1.当前熟了的食物的最小id 2.当前熟了的食物中有没有编号为id的食物 3.当前没熟的食物中有没有编号为id的食物 4.当前没 ...

  6. iOS从零开始 Code Review

    http://www.cocoachina.com/ios/20151117/14208.html 这篇帖子不是通篇介绍Code Review的方法论, 而是前大段记录了我们团队怎么从没有这个习惯到每 ...

  7. C#中App.config文件配置获取

    最新的framework使用如下方法: using System.Configuration; ConfigurationManager.AppSettings["key"]; A ...

  8. 每天一个linux命令(1): which命令

    0.学习时间: 2014-05-15 which命令用来在PATH指定的路径中查找特定的文件, 并返回第一个找到的结果. 1. 命令格式:  which 文件名 2.命令功能 一般使用which命令来 ...

  9. 【Leetcode堆】数据流中的第K大元素(703)

    题目 设计一个找到数据流中第K大元素的类(class).注意是排序后的第K大元素,不是第K个不同的元素. 你的 KthLargest 类需要一个同时接收整数 k 和整数数组nums 的构造器,它包含数 ...

  10. WebSocket简述

    WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议. WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据.在 W ...