文章目录
- 14.muduo的线程模型
- 13.用eventfd唤醒Eventloop::loop()中的poll阻塞调用
- 12. 在线程间调配任务: runInLoop(const Functor& cb)函数
- 11.fork()创建进程
- 10.eventfd
- 9.back_inserter(container),front_inserter(container),inserter(container,pos)
- 8. lower_bound和upper_bound
- 7. boost::scoped_ptr
- 6.linux新定时器:timefd及相关操作函数
- 5. S01中 eventloop,poller,channel之间的关系
- 4. 一个线程只允许创建一个loop(one thread one loop)
- 3. __thread 关键字
- 2.获取当前线程ID
- 1. __builtin_expect 分支预测优化
14.muduo的线程模型
原文:Muduo网络库源代码分析(四)EventLoopThread和EventLoopThreadPool的封装
mainReactor关注监听事件,已连接套接字事件轮询给线程池中的subReactors 处理,一个新的连接相应一个subReactor
13.用eventfd唤醒Eventloop::loop()中的poll阻塞调用
首先要明白为什么会有需要唤醒的问题. 原因是: IO线程平时都阻塞在事件循环的poll(epoll)调用上,为了让IO线程能立即执行用户的回调,所以需要去唤醒它.
步骤1: wakeupChannel_的创建
先是在Eventloop的够造函数中创建了 wakeupFd_ ,然后用它创建了 wakeupChannel_,并且设置了这个channel的回调函数,然后通过enablereading()这个函数层层将wakeupChannel_加入到散列表channels_中
EventLoop::EventLoop()
: ...
wakeupFd_(createEventfd()),
wakeupChannel_(new Channel(this, wakeupFd_)){
...
wakeupChannel_->setReadCallback(boost::bind(&EventLoop::handleRead, this));//其中handleRead就是唤醒后的调用
// we are always reading the wakeupfd
wakeupChannel_->enableReading();
}
void EventLoop::handleRead()
{
uint64_t one = 1;
ssize_t n = ::read(wakeupFd_, &one, sizeof one);
if (n != sizeof one)
{
LOG_ERROR << "EventLoop::handleRead() reads " << n << " bytes instead of 8";
}
}
static int createEventfd(){
int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (evtfd < 0){
LOG_SYSERR << "Failed in eventfd";
abort();}
return evtfd;
}
步骤2:wakeupChannel_加入事件循环并等待执行
将wakeupChannel_加入到散列表channels_中的调用关系如下:
void enableReading() { events_ |= kReadEvent; update(); }
void Channel::update(){
loop_->updateChannel(this);
}
void EventLoop::updateChannel(Channel* channel){
assert(channel->ownerLoop() == this);
assertInLoopThread();
poller_->updateChannel(channel);
}
void Poller::updateChannel(Channel* channel){
assertInLoopThread();
LOG_TRACE << "fd = " << channel->fd() << " events = " << channel->events();
if (channel->index() < 0) {
// a new one, add to pollfds_
assert(channels_.find(channel->fd()) == channels_.end());
struct pollfd pfd;
pfd.fd = channel->fd();
pfd.events = static_cast<short>(channel->events());
pfd.revents = 0;
...
channels_[pfd.fd] = channel;
...
}
最终所有活动的channel都会被加入activeChannels_中,并在EventLoop::loop()的循环中得以执行.
void EventLoop::loop(){
...
while (!quit_){
activeChannels_.clear();
poller_->poll(kPollTimeMs, &activeChannels_);
for (ChannelList::iterator it = activeChannels_.begin();it != activeChannels_.end(); ++it){
(*it)->handleEvent();
}
}
...
}
步骤3.事件唤醒
唤醒的方法很简单,只要往wakeupFd_描述符中写入一个字符即可.
void EventLoop::wakeup(){
uint64_t one = 1;
ssize_t n = ::write(wakeupFd_, &one, sizeof one);
if (n != sizeof one){
LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
}
}
在什么时候去唤醒呢?
首先要明白为什么会有需要唤醒的问题. 原因是: IO线程平时都阻塞在事件循环的poll(epoll)调用上,为了让IO线程能立即执行用户的回调,所以需要去唤醒它. 所以 :
1.如果调用queueInloop的线程不是IO线程(即不是当前线程,我的理解代表可能用户有其他任务需要执行)需要唤醒,这中情况与极客时间<<网络编程实战>>中主线程只管接收连接并将事件fd注册到子线程时,需要唤醒子线程的情况类似. 对应if (!isInLoopThread() || callingPendingFunctors_) 中 !isInLoopThread() 这个条件
2.如果IO线程调用queueInloop,而此时正在调用pending functor ( doPendingFunctors() ) 也必须唤醒.(doPendingFunctors()调用的Functor可能再调用queueInLoop(cb),这时queueInLoop()必须唤醒wakeup(),否则这些新加入的cb就不能被及时调用了, 对应if (!isInLoopThread() || callingPendingFunctors_) 中 callingPendingFunctors_ 这个条件)
书中最后概括为:只有在IO线程的事件回调中调用queueInloop不需要唤醒
上面分析对应代码
非当前线程调用runInLoop时
void EventLoop::runInLoop(const Functor& cb){
if (isInLoopThread()){
cb();
}
else{
queueInLoop(cb);
}
}
void EventLoop::queueInLoop(const Functor& cb){{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(cb);
}
if (!isInLoopThread() || callingPendingFunctors_) {
wakeup();
}}
void EventLoop::doPendingFunctors(){
std::vector<Functor> functors;
callingPendingFunctors_ = true;
{
MutexLockGuard lock(mutex_);
functors.swap(pendingFunctors_);
}
for (size_t i = 0; i < functors.size(); ++i){
functors[i]();}
callingPendingFunctors_ = false;
}
3.因为quit()通过将标记位quit_置为true来退出,不是立即生效的,如果是非当前IO线程调用了quit(),而当前线程正阻塞在poll(epoll)调用上,延时可能长达数秒(由poll或epoll的超时时间决定,也可能不会退出),所以可通过唤醒来立即退出.
第二:Eventloop退出时
void EventLoop::quit(){
quit_ = true;
if (!isInLoopThread()){
wakeup();
}
}
12. 在线程间调配任务: runInLoop(const Functor& cb)函数
Eventloop可以通过runInLoop这个函数轻易地在线程间调配任务,其中Functor是boost::function<void()>.如果用户在当前IO线程中调用这个函数,回调会立即这行,如果在其他线程中调用这个函数,回调cb函数会被加入队列,由事件循环来执行.
bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
void EventLoop::runInLoop(const Functor& cb){
if (isInLoopThread()){//判断是否当前IO线程
cb();}
else{
queueInLoop(cb);}}
void EventLoop::queueInLoop(const Functor& cb){
{ MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(cb);
}
if (!isInLoopThread() || callingPendingFunctors_){
wakeup();}}
pendingFunctors_在doPendingFunctors()函数中进行了调用,注意这里pendingFunctors_可能被多个线程操作,所以要加锁,
**注意:
- 这里采用栈上中间变量functors(栈上对象不用担心线程安全)与pendingFunctors_进行交换的方式来缩小临界区.(意味着不会阻塞其他线程调用queueInLoop()).
- 另一方面也避免了死锁,因为functor是函数封装,它可能会再去调用queueInLoop**
void EventLoop::doPendingFunctors(){
std::vector<Functor> functors;
callingPendingFunctors_ = true;
{
MutexLockGuard lock(mutex_);
functors.swap(pendingFunctors_);
}
for (size_t i = 0; i < functors.size(); ++i){
functors[i]();}
callingPendingFunctors_ = false;
}
这个函数的调用位置为:
void EventLoop::loop(){
...
while (!quit_){
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (ChannelList::iterator it = activeChannels_.begin();it != activeChannels_.end(); ++it){
(*it)->handleEvent();
}
doPendingFunctors();
}
...
}
可能发生死锁的情况如下:
//可能死锁为情况:如果(B)处就是上面的run2,(A处已经拿到了锁,(B)又视图去拿同一把锁就会发生死锁)
void run2(){
printf("run2(): pid = %d, flag = %d\n", getpid(), g_flag);
g_loop->queueInLoop(run3);
}
void run1(){
g_flag = 1;
printf("run1(): pid = %d, flag = %d\n", getpid(), g_flag);
g_loop->runInLoop(run2);
g_flag = 2;
}
void EventLoop::doPendingFunctors(){
callingPendingFunctors_ = true;
MutexLockGuard lock(mutex_);// (A)
for (size_t i = 0; i < functors.size(); ++i){
pendingFunctors_[i]();}//(B) 如果这里就是上面的run2
callingPendingFunctors_ = false;}
总结一下就是:
假设我们有这样的调用:loop->runInLoop(run),说明想让IO线程执行一定的计算任务,此时若是在当前的IO线程,就马上执行run();如果是其他线程调用的,那么就执行queueInLoop(run),将run异步添加到队列,当loop内处理完事件后,就执行doPendingFunctors(),也就执行到了run();
参考:原文链接
11.fork()创建进程
fork的使用例子如下所示:
int main(int c,char **v){
int listen_fd=tcp_server_listen(SERV_PORT);
signal(SIGCHLD,sigchld_handler);
while(1){
struct sockaddr_storage ss;
socklen_t slen= sizeof(ss);
int conn_fd=accept(listen_fd,(struct sockaddr*)&ss,&slen);
if(conn_fd<0){
error(1,errno,"accept failed");
exit(1);
}
/*
* 从父进程派生出的子进程,同时也会复制一份描述字,也就是说,连接套接字和
* 监听套接字的引用计数都会被加 1,而调用 close 函数则会对引用计数进行减 1 操作,
* 这样在套接字引用计数到 0 时,才可以将套接字资源回收。所以,这里的 close
* 函数非常重要,缺少了它们,就会引起服务器端资源的泄露。*/
//fork先返回非零值,再返回零值
if(fork()==0){//子进程,只负责连接套接字
//子进程不需要关心监听套接字,故而在这里关闭掉监听套接字 listen_fd
close(listen_fd);
printf("child process\n");
child_run(conn_fd);
exit(0);
}else{ //父进程,只负责监听套接字
printf("this is main process\n");
//父进程不需要关心连接套接字,所以在这里关闭连接套接字
close(conn_fd);
}
}
return 0;
}
其他的辅助代码如下:
#include<iostream>
#include <poll.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
#include <error.h>
#include <unistd.h>
#include "message_objecte.h"
#include <signal.h>
#include <wait.h>
#define SERV_PORT 43211
#define MAXLINE 4096
#define UNIXSTR_PATH "/var/lib/unixstream1.sock"
#define LISTENQ 1024
#define BUFFER_SIZE 4096
#define MAXLINE 4096
int tcp_server_listen(u_int32_t port){
//1.create
int fd=socket(AF_INET,SOCK_STREAM,0);
//2.init
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_port=htons(port);
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=INADDR_ANY;
//3.set reuse(SOL_SOCKET 套接字级别)
int on=1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
//4.bind
socklen_t len= sizeof(server_addr);
int rt1=bind(fd,(struct sockaddr*)&server_addr,len);
if(rt1<0){
error(1,errno,"bind failed");
}
//5.listen
int rt2=listen(fd,1024);
if(rt2<0){
error(1,errno,"listen failed");
}
return fd;
}
void child_run(int fd){
char outbuf[MAXLINE+1];
size_t outbuf_used=0;
ssize_t result;
while(1){
char ch;
//这里的recv是阻塞模式的
result = recv(fd,&ch,1,0);//一次接收一个字符
//如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
if(result == 0){
printf("client closed\n");
break;
}else if(result==-1){
perror("read");
break;
}
if(outbuf_used< sizeof(outbuf)){
outbuf[outbuf_used++]=ch;
}
if(ch=='\n'){
send(fd,outbuf,outbuf_used,0);
outbuf_used=0;
continue;
}
}
}
//处理子进程退出的方式一般是注册一个信号处理函数,
// 捕捉信号 SIGCHILD 信号,然后再在信号处理函数里
// 调用 waitpid 函数来完成子进程资源的回收。
void sigchld_handler(int sig)
{
//这里选项 WNOHANG 用来告诉内核,即使还有未终止的子进程也不要阻塞
// 在 waitpid 上。注意这里不可以使用 wait,
// 因为 wait 函数在有未终止子进程的情况下,没有办法不阻塞。
printf("quit child process\n");
while(waitpid(-1,0,WNOHANG)>0){
return;
}
}
10.eventfd
什么是eventfd
eventfd是Linux 2.6提供的一种系统调用,它可以用来实现事件通知。eventfd包含一个由内核维护的64位无符号整型计数器,创建eventfd时会返回一个文件描述符,进程可以通过对这个文件描述符进行read/write来读取/改变计数器的值,从而实现进程间通信。
创建eventfd
eventfd的创建是通过eventfd函数实现的,返回值即是该eventfd所对应的文件描述符,函数的原型如下所示:
initval:创建eventfd时它所对应的64位计数器的初始值;
flags:eventfd文件描述符的标志,可由三种选项组成:EFD_CLOEXEC、EFD_NONBLOCK 和 EFD_SEMAPHORE。
EFD_CLOEXEC 表示返回的eventfd文件描述符在fork后exec其他程序时会自动关闭这个文件描述符;
EFD_NONBLOCK设置返回的eventfd非阻塞;
EFD_SEMAPHORE 表示将eventfd作为一个信号量来使用。
9.back_inserter(container),front_inserter(container),inserter(container,pos)
三种迭代器配接器(iterator Adapters):(1)insert iterators(安插型迭代器)(2)stream iterators (流迭代器)(3)reverse iterators (逆向迭代器)详细信息可参考《c++ 标准程序库》第7.4节。
详细介绍下:
1.安插型迭代器(insert iterators)
1)back_inserter(container):使用push_back()在容器尾端安插元素,元素排列顺序和安插顺序相同。只有在提供了push_back()成员函数的容器才能使back_inserter(container)这样的容器有:vector,deque,list
2)front_inserter(container):在内部调用push_front()成员函数,将元素安插于容器中最前端。采用头插法插入元素,数据元素在容器中的位置和插入时的顺序刚好相反。同样,只有提供了push_front()成员函数的容器才能使用 front_inserter(container)这样的迭代器有:deque,list.
3)inserter(container,pos):在内部调用insert()成员函数,将元素插入第二个参数所指的位置。因为在stl所有的容器中都包含有insert()成员函数,所以所有的容器包括关联式容器都能够使用 inserter(container, pos).但是,我们知道关联式容器中数据元素是有序的,数据元素在容器中的位置只是和元素值有关。在关联式容器中,提供一个迭代器只是告诉容器确定从什么地方开始搜寻正确的位置,如果提示不正确的话,效率比没有提示更糟,所以对关联式容器来说,我们必须慎重
8. lower_bound和upper_bound
lower_bound和upper_bound是C++ STL中提供的非常实用的函数。其操作对象可以是vector、set以及map。lower_bound返回值一般是>= 给定val的最小指针(iterator)。upper_bound返回值则是 > 给定val的最小指针(iterator)原文链接
http://www.cplusplus.com/reference/set/set/lower_bound/
7. boost::scoped_ptr
scoped_ptr 有着与std::auto_ptr类似的特性(独占指针),而最大的区别在于它不能转让所有权而auto_ptr可以。事实上,scoped_ptr永远不能被复制或被赋值!auto_ptr、shared_ptr、weak_ptr、scoped_ptr用法小结
6.linux新定时器:timefd及相关操作函数
#include<time.h>
int clock_gettime(clockid_t clk_id, struct timespec *tp); clk_id 检索和设置的clk_id指定的时钟时间
CLOCK_REALTIME:系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0开始计时,中间时刻如果系统时间被用户改成其他,则对应的时间相应改变
CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码系统CPU花费的时间
CLOCK_THREAD_CPUTIME_ID:本线程到当前代码系统CPU花费的时间
#if 1
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h> /* Definition of uint64_t */
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
static void print_elapsed_time(void){
static struct timespec start;
struct timespec curr;
static int first_call = 1;
int secs, nsecs;
if (first_call) {
first_call = 0;
if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
handle_error("clock_gettime");}
if (clock_gettime(CLOCK_MONOTONIC, &curr) == -1)
handle_error("clock_gettime");
secs = int(curr.tv_sec - start.tv_sec);
nsecs = int(curr.tv_nsec - start.tv_nsec);
if (nsecs < 0) {
secs--;
nsecs += 1000000000; }
printf("%d.%03d: ", secs, (nsecs + 500000) / 1000000);
}
int main(int argc, char *argv[])
{
struct itimerspec new_value;
int max_exp, fd;
struct timespec now;
int exp, tot_exp;
ssize_t s;
if (clock_gettime(CLOCK_REALTIME, &now) == -1)
handle_error("clock_gettime");
new_value.it_value.tv_sec = now.tv_sec + atoi("3");//第一次3秒后超时
new_value.it_value.tv_nsec = now.tv_nsec;
new_value.it_interval.tv_sec = atoi("1");//每隔1秒超时一次
new_value.it_interval.tv_nsec = 0;
//最多超时3次
max_exp = atoi("3");
//int timerfd_create(int clockid, int flags);
//它是用来创建一个定时器描述符timerfd
//第一个参数:clockid指定时间类型,有两个值:
//CLOCK_REALTIME :Systemwide realtime clock. 系统范围内的实时时钟
//CLOCK_MONOTONIC:以固定的速率运行,从不进行调整和复位 ,它不受任何系统time-of-day时钟修改的影响
//第二个参数:flags可以是0或者O_CLOEXEC/O_NONBLOCK。
//返回值:timerfd(文件描述符)
fd = timerfd_create(CLOCK_REALTIME, 0);
if (fd == -1)
handle_error("timerfd_create");
// struct itimerspec {
// struct timespec it_interval; /* Interval for periodic timer */
// struct timespec it_value; /* Initial expiration */
// };
// 第二个结构体itimerspec就是timerfd要设置的超时结构体,它的成员it_value表示定时器第一次超时时间,
// it_interval表示之后的超时时间即每隔多长时间超时
if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1)
handle_error("timerfd_settime");
print_elapsed_time();
printf("timer started\n");
for (tot_exp = 0; tot_exp < max_exp;) {
s = read(fd, &exp, sizeof(uint64_t));
if (s != sizeof(uint64_t))
handle_error("read");
tot_exp += exp;
print_elapsed_time();
printf("read: %d; total=%d\n",exp,tot_exp);
}
exit(EXIT_SUCCESS);
}
#endif
0.000: timer started
3.000: read: 1; total=1
4.000: read: 1; total=2
5.000: read: 1; total=3
5. S01中 eventloop,poller,channel之间的关系
s01demo中 eventloop,poller,channel三者之间个关系如下图所示.即
1.eventloop包含了poller,虽然是指针成员,但是pollers不可复制,不会这个demo中poller也没有暴露出去,所以生命周期和eventloop一样(用了实心组合关系)
2.其余的关系就都是一些关联关系了
(这个图画了之后感觉反而不是很清晰了,就当练习吧,这块儿的梳理意义不大)
4. 一个线程只允许创建一个loop(one thread one loop)
muduo中的实现方式如下面代码所示,可以看出是通过一个__thread 变量实现的,__thread变量的特点是:
1.每个线程独占一个备份;2.多个线程之间互不干扰
__thread EventLoop* t_loopInThisThread = 0;
EventLoop::EventLoop()
: looping_(false),
threadId_(CurrentThread::tid()){
LOG_TRACE << "EventLoop created " << this << " in thread " << threadId_;
if (t_loopInThisThread){
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
}
else{
t_loopInThisThread = this;
}
}
3. __thread 关键字
_thread是GCC内置的线程局部存储设施,存取效率可以和全局变量相比。__thread变量每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。
__thread使用规则:只能修饰POD类型(类似整型指针的标量,不带自定义的构造、拷贝、赋值、析构的类型,二进制内容可以任意复制memset,memcpy,且内容可以复原),不能修饰class类型,因为无法自动调用构造函数和析构函数,可以用于修饰全局变量,函数内的静态变量,不能修饰函数的局部变量或者class的普通成员变量,且__thread变量值只能初始化为编译器常量(值在编译器就可以确定const int i=5,运行期常量是运行初始化后不再改变const int i=rand()).
原文链接
下面简单的事例中,总共有三个线程(包括主线程),可以看到通过__thread 修饰的变量,
在线程中地址都不一样,__thread变量每一个线程有一份独立实体,各个线程的值互不干扰。
#include <pthread.h>
#include <unistd.h>
#include <iostream>
using namespace std;
//(1)(2)两种方式效果一样
//__thread int var=5;(1)
const int i=5;
__thread int var=i;//(2)
static __thread int var2 = 15;//
static void* worker1(void* arg);
static void* worker2(void* arg);
int main(){
pthread_t pid1,pid2;
static __thread int temp=10;//修饰函数内的static变量
pthread_create(&pid1,NULL,worker1,NULL);
pthread_create(&pid2,NULL,worker2,NULL);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
cout<<"main var addr :" << &var<<endl;
cout<<"main var2 addr :" << &var2<<endl;
cout<<temp<<endl;//输出10
return 0;
}
static void* worker1(void* arg){
cout<<"worker1 var :" << ++var<<endl;//6
cout<<"worker1 var addr :" << &var<<endl;
cout<<"worker1 var2 :" << ++var2<<endl;//16
cout<<"worker1 var2 addr :" << &var2<<endl;
return NULL;
}
static void* worker2(void* arg){
sleep(1);//等待线程1改变var值,验证是否影响线程2
cout<< "worker2 var :" << --var<<endl;//4
cout<<"worker2 var addr :" << &var<<endl;
cout<<"worker2 var2 :" << --var2<<endl;//14
cout<<"worker2 var2 addr :" << &var2<<endl;
return NULL;
}
//worker1 var :6
//worker1 var addr :0x7f941d5df6f4
//worker1 var2 :16
//worker1 var2 addr :0x7f941d5df6f8
//worker2 var :4
//worker2 var addr :0x7f941cdde6f4
//worker2 var2 :14
//worker2 var2 addr :0x7f941cdde6f8
//main var addr :0x7f941e717734
//main var2 addr :0x7f941e717738
//10
//上面简单的事例中,总共有三个线程(包括主线程),可以看到通过__thread 修饰的变量,
//在线程中地址都不一样,__thread变量每一个线程有一份独立实体,各个线程的值互不干扰。
2.获取当前线程ID
muduo中获取当前线程ID的方法为:
muduo::CurrentThread::tid()
查看源码后发现最终的实现为: static_cast<pid_t>(::syscall(SYS_gettid));
#include <sys/syscall.h>//SYS_gettid
int gettid(){
//测试 muduo::CurrentThread::tid() 的实现
return static_cast<pid_t>(::syscall(SYS_gettid));
}
1. __builtin_expect 分支预测优化
0.使用方法
与关键字if一起使用.首先要明确一点就是 if (value) 等价于 if (__builtin_expert(value, x)), 与x的值无关.
1.引言
在很多源码如Linux内核、Glib等,我们都能看到likely()和unlikely()这两个宏,通常这两个宏定义是下面这样的形式。
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
可以看出这2个宏都是使用函数 __builtin_expect()实现的, __builtin_expect()函数是GCC的一个内建函数(build-in function).
- 函数声明
函数__builtin_expect()是GCC v2.96版本引入的, 其声明如下:
long __builtin_expect(long exp, long c);
2.1. 功能描述
由于大部分程序员在分支预测方面做得很糟糕,所以GCC 提供了这个内建函数来帮助程序员处理分支预测.
你期望 exp 表达式的值等于常量 c, 看 c 的值, 如果 c 的值为0(即期望的函数返回值), 那么 执行 if 分支的的可能性小, 否则执行 else 分支的可能性小(函数的返回值等于第一个参数 exp).
GCC在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降, 达到优化程序的目的.
通常,你也许会更喜欢使用 gcc 的一个参数 ‘-fprofile-arcs’ 来收集程序运行的关于执行流程和分支走向的实际反馈信息,但是对于很多程序来说,数据是很难收集的。
2.2. 参数详解
① exp
exp 为一个整型表达式, 例如: (ptr != NULL)
② c
c 必须是一个编译期常量, 不能使用变量
2.3. 返回值
返回值等于 第一个参数 exp
2.4. 使用方法
与关键字if一起使用.首先要明确一点就是 if (value) 等价于 if (__builtin_expert(value, x)), 与x的值无关.
例子如下:
例子1 : 期望 x == 0, 所以执行func()的可能性小
复制代码
if (__builtin_expect(x, 0))
{
func();
}
else
{
//do someting
}
例子2 : 期望 ptr !=NULL这个条件成立(1), 所以执行func()的可能性小
if (__builtin_expect(ptr != NULL, 1))
{
//do something
}
else
{
func();
}
例子3 : 引言中的likely()和unlikely()宏
首先,看第一个参数**!!(x),** 他的作用是把(x)转变成"布尔值", 无论(x)的值是多少 !(x)得到的是true或false, !!(x)就得到了原值的"布尔值"
使用 likely() ,执行 if 后面的语句 的机会更大,使用 unlikely(),执行 else 后面的语句的机会更大。
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
int main(char *argv[], int argc)
{
int a;
/* Get the value from somewhere GCC can't optimize */
a = atoi (argv[1]);
if (unlikely (a == 2)) {
a++;
}
else{
a--;
}
printf ("%d\n", a);
return 0;
}
- RATIONALE(原理)
if else 句型编译后, 一个分支的汇编代码紧随前面的代码,而另一个分支的汇编代码需要使用JMP指令才能访问到.
很明显通过JMP访问需要更多的时间, 在复杂的程序中,有很多的if else句型,又或者是一个有if else句型的库函数,每秒钟被调用几万次,
通常程序员在分支预测方面做得很糟糕, 编译器又不能精准的预测每一个分支,这时JMP产生的时间浪费就会很大,
函数__builtin_expert()就是用来解决这个问题的.
具体从汇编角度来分析其原理的例子,大家可以参照http://kernelnewbies.org/FAQ/LikelyUnlikely,
其对应的中文翻译版见http://velep.com/archives/795.html