linux学习笔记-muduo网络库设计与实现

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_可能被多个线程操作,所以要加锁,
**注意:

  1. 这里采用栈上中间变量functors(栈上对象不用担心线程安全)与pendingFunctors_进行交换的方式来缩小临界区.(意味着不会阻塞其他线程调用queueInLoop()).
  2. 另一方面也避免了死锁,因为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

Linux进程间通信——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及相关操作函数

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 分支预测优化

原文连接:__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).

  1. 函数声明
    函数__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;
}
  1. RATIONALE(原理)
    if else 句型编译后, 一个分支的汇编代码紧随前面的代码,而另一个分支的汇编代码需要使用JMP指令才能访问到.
    很明显通过JMP访问需要更多的时间, 在复杂的程序中,有很多的if else句型,又或者是一个有if else句型的库函数,每秒钟被调用几万次,
    通常程序员在分支预测方面做得很糟糕, 编译器又不能精准的预测每一个分支,这时JMP产生的时间浪费就会很大,
    函数__builtin_expert()就是用来解决这个问题的.
    具体从汇编角度来分析其原理的例子,大家可以参照http://kernelnewbies.org/FAQ/LikelyUnlikely,
    其对应的中文翻译版见http://velep.com/archives/795.html
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值