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. win10 基础上装一个 ubuntu 双系统

    1.准备一个空闲的分区: 1)确定每个磁盘都已经是 EFI 分区格式,如果不是,可以使用分区工具,将分区都转成EFI  (例如 DiskGenius 工具挺好) 2)选择一个剩余空间较大的压缩空间,压 ...

  2. git中由readme.md文件引发的问题

    在GitHub上建立一个仓库并且添加了readme.txt文件. 无论是push前先将远程仓库pull到本地仓库,还是强制push都会弹出这个问题. Github 禁用了TLS v1.0 and v1 ...

  3. zk运维注意事项

    1 连接数容易占满 2 watches数 (应用上的比较多建议做个自动监控,告警)

  4. chmod 777 修改权限之后,文件夹颜色变绿:解决方案

    修改前: 方法一: ls --color=none #不显示颜色 方法一修改后: 方法二:修改配色 ①安装git +++可用在windows下载之后用ftp传上去:http://pan.baidu.c ...

  5. ML面试1000题系列(81-90)

    本文总结ML面试常见的问题集 转载来源:https://blog.csdn.net/v_july_v/article/details/78121924 81.已知一组数据的协方差矩阵P,下面关于主分量 ...

  6. hdu 1358 Period(KMP入门题)

    Period Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Subm ...

  7. LintCode_175 翻转二叉树

    题目 翻转一棵二叉树 您在真实的面试中是否遇到过这个题? Yes 样例 1 1 / \ / \ 2 3 => 3 2 / \ 4 4 和前序遍历代码很相似从叶子节点依次翻转递归到根节点C++代码 ...

  8. Dubbo+JStorm

    Dubbo,是阿里巴巴服务化治理的核心框架,并被广泛应用于阿里巴巴集团的各成员站点.阿里巴巴近几年对开源社区的贡献不论在国内还是国外都是引人注目的,比如:JStorm捐赠给Apache并加入Apach ...

  9. Leetcode680.Valid Palindrome II验证回文字符串2

    给定一个非空字符串 s,最多删除一个字符.判断是否能成为回文字符串. 示例 1: 输入: "aba" 输出: True 示例 2: 输入: "abca" 输出: ...

  10. Android LRUCache简介

    LRU Cache数据结构的介绍可以参考前面的http://www.cnblogs.com/XP-Lee/p/3441555.html. 本文以Android LRUCache来做一个简单的介绍.我们 ...