Asio源码分析(2):Asio用到的C++技巧和优化

Asio源码分析 专栏收录该内容
3 篇文章 0 订阅

Asio源码分析(2):Asio用到的C++技巧和优化

Asio中用到了一些C++技巧和优化,第一次阅读源码时可能会有一些困惑(confuse)。这篇文章先简单介绍其中的一小部分,方便后面的分析。

使用析构函数执行清理操作

Asio使用对象的析构函数在作用域结束时执行一些清理操作,有点类似于Go语言中的defer

例如,Asio会维护一个线程间共享的Operation Queue,Asio还维护一个线程私有的Operation Queue,在运行时会先把已完成的Operation放到私有队列中,最后再将私有的Operation Queue中的Operation转移到共享的Operation Queue中:

struct scheduler::task_cleanup {
    ~task_cleanup() {
        if (this_thread_->private_outstanding_work > 0) {
            asio::detail::increment(
                scheduler_->outstanding_work_,
                this_thread_->private_outstanding_work);
        }
        this_thread_->private_outstanding_work = 0;

        // Enqueue the completed operations and reinsert the task at the end of
        // the operation queue.
        lock_->lock();
        scheduler_->task_interrupted_ = true;
        scheduler_->op_queue_.push(this_thread_->private_op_queue);
        scheduler_->op_queue_.push(&scheduler_->task_operation_);
    }

    scheduler* scheduler_;
    mutex::scoped_lock* lock_;
    thread_info* this_thread_;
};

该struct使用于(忽略了一些无关代码):

std::size_t scheduler::do_run_one(mutex::scoped_lock& lock,
                                  scheduler::thread_info& this_thread,
                                  const asio::error_code& ec) {
    while (!stopped_) {
        if (!op_queue_.empty()) {
            // Prepare to execute first handler from queue.
            operation* o = op_queue_.front();
            op_queue_.pop();
            // ...

            if (o == &task_operation_) {
                // ...

                task_cleanup on_exit = {this, &lock, &this_thread};
                (void)on_exit;

                // Run the task. May throw an exception. Only block if the operation
                // queue is empty and we're not polling, otherwise we want to return
                // as soon as possible.
                task_->run(more_handlers ? 0 : -1, this_thread.private_op_queue);
            } else {
                // ...
            }
        } else {
            // ...
        }
    }

    return 0;
}

其实这里不使用析构函数能也实现同样的行为,但Asio中普遍使用了析构函数来执行清理工作。

使用模板元编程模拟概念

在C++20中,我们可以通过概念(Concepts)对模板实参做出约束。但在C++20之前没有这个特性,所以通常会使用模板元编程来模拟:

template<typename Iter>
    /*requires*/ enable_if<random_access_iterator<Iter>, void>
advance(Iter p, int n) { p += n; }

template<typename Iter>
    /*requires*/ enable_if<forward_iterator<Iter>, void>
advance(Iter p, int n) { assert(n >= 0); while (n--) ++p;}

使用模板元编程在编译时求值已经被constexpr函数替代了,现在模板元编程主要的作用为在编译时操纵类型。

使用对象池避免对象频繁地创建和销毁

在Linux下使用epoll时,每个fd都有一个descriptor status,它们放在epoll_dataptr字段上:

// Per-descriptor queues.
class descriptor_state : operation {
    friend class epoll_reactor;
    friend class object_pool_access;

    descriptor_state* next_;
    descriptor_state* prev_;

    mutex mutex_;
    epoll_reactor* reactor_;
    int descriptor_;
    uint32_t registered_events_;
    op_queue<reactor_op> op_queue_[max_ops];
    bool try_speculative_[max_ops];
    bool shutdown_;

    ASIO_DECL descriptor_state(bool locking);
    void set_ready_events(uint32_t events) { task_result_ = events; }
    void add_ready_events(uint32_t events) { task_result_ |= events; }
    ASIO_DECL operation* perform_io(uint32_t events);
    ASIO_DECL static void do_complete(
        void* owner, operation* base,
        const asio::error_code& ec, std::size_t bytes_transferred);
};

每当创建一个socket时,Asio都要创建一个关联的descriptor status。对象池可以避免对象频繁创建和销毁,因为它不会释放已死亡对象的内存,而是重复利用这些内存。

Asio中有一个object_pool<Object>类用于管理正在使用和已经释放的对象。它的实现并不复杂,具体实现为维护两条Object的链表,一条live_list_存放正在使用的对象,一条free_list_存放已经释放的对象(它可以再次被使用)。

当需要创建Object时,先看一下free_list_上有没有对象可以使用,如果有则直接返回,并将它添加到live_list_;如果没有则创建一个,并将它添加到live_list_

Object销毁时,将该对象从live_list_上移除,并添加到free_list_

下面是object_pool的完整代码:

namespace asio {
namespace detail {

template <typename Object>
class object_pool;

class object_pool_access {
public:
    template <typename Object>
    static Object* create() {
        return new Object;
    }

    template <typename Object, typename Arg>
    static Object* create(Arg arg) {
        return new Object(arg);
    }

    template <typename Object>
    static void destroy(Object* o) {
        delete o;
    }

    template <typename Object>
    static Object*& next(Object* o) {
        return o->next_;
    }

    template <typename Object>
    static Object*& prev(Object* o) {
        return o->prev_;
    }
};

template <typename Object>
class object_pool : private noncopyable {
public:
    // Constructor.
    object_pool()
        : live_list_(0),
          free_list_(0) {
    }

    // Destructor destroys all objects.
    ~object_pool() {
        destroy_list(live_list_);
        destroy_list(free_list_);
    }

    // Get the object at the start of the live list.
    Object* first() {
        return live_list_;
    }

    // Allocate a new object.
    Object* alloc() {
        Object* o = free_list_;
        if (o)
            free_list_ = object_pool_access::next(free_list_);
        else
            o = object_pool_access::create<Object>();

        object_pool_access::next(o) = live_list_;
        object_pool_access::prev(o) = 0;
        if (live_list_)
            object_pool_access::prev(live_list_) = o;
        live_list_ = o;

        return o;
    }

    // Allocate a new object with an argument.
    template <typename Arg>
    Object* alloc(Arg arg) {
        Object* o = free_list_;
        if (o)
            free_list_ = object_pool_access::next(free_list_);
        else
            o = object_pool_access::create<Object>(arg);

        object_pool_access::next(o) = live_list_;
        object_pool_access::prev(o) = 0;
        if (live_list_)
            object_pool_access::prev(live_list_) = o;
        live_list_ = o;

        return o;
    }

    // Free an object. Moves it to the free list. No destructors are run.
    void free(Object* o) {
        if (live_list_ == o)
            live_list_ = object_pool_access::next(o);

        if (object_pool_access::prev(o)) {
            object_pool_access::next(object_pool_access::prev(o)) = object_pool_access::next(o);
        }

        if (object_pool_access::next(o)) {
            object_pool_access::prev(object_pool_access::next(o)) = object_pool_access::prev(o);
        }

        object_pool_access::next(o) = free_list_;
        object_pool_access::prev(o) = 0;
        free_list_ = o;
    }

private:
    // Helper function to destroy all elements in a list.
    void destroy_list(Object* list) {
        while (list) {
            Object* o = list;
            list = object_pool_access::next(o);
            object_pool_access::destroy(o);
        }
    }

    // The list of live objects.
    Object* live_list_;

    // The free list.
    Object* free_list_;
};

} // namespace detail
} // namespace asio

使用函数指针和继承实现多态

使用虚函数实现的多态会有运行时的开销(这样的开销不仅是查询虚函数表)。对于operation,正如之前所说,他会在程序中被频繁地使用,而它又是一个接口类(operation有一个complete()方法,具体到不同派生出的operation会有不同的实现方式)。

Asio在operation中存放了一个函数指针,子类在构造时将具体的completion方法传递给operation的构造函数,然后在operationcomplete()方法中通过该函数指针,即可调用子类指定的函数:

namespace asio {
namespace detail {

#if defined(ASIO_HAS_IOCP)
typedef win_iocp_operation operation;
#else
typedef scheduler_operation operation;
#endif

} // namespace detail
} // namespace asio
// Base class for all operations. A function pointer is used instead of virtual
// functions to avoid the associated overhead.
class scheduler_operation ASIO_INHERIT_TRACKED_HANDLER {
public:
    typedef scheduler_operation operation_type;

    void complete(void* owner, const asio::error_code& ec,
                  std::size_t bytes_transferred) {
        func_(owner, this, ec, bytes_transferred);
    }

    void destroy() {
        func_(0, this, asio::error_code(), 0);
    }

protected:
    typedef void (*func_type)(void*,
                              scheduler_operation*,
                              const asio::error_code&, std::size_t);

    scheduler_operation(func_type func)
        : next_(0),
          func_(func),
          task_result_(0) {}

    // Prevents deletion through this type.
    ~scheduler_operation() {}

private:
    friend class op_queue_access;
    scheduler_operation* next_;
    func_type func_;

protected:
    friend class scheduler;
    unsigned int task_result_; // Passed into bytes transferred.
};

我们可以验证一下这样做是否比虚函数性能更好(开启O2优化):

函数指针
虚函数

可以看到使用函数指针的方式能被内联展开,优化到只有一条std::cout语句。而使用虚函数的方式编译器无法内联展开。

使用继承共享数据而不是实现多态(虚函数)

继承的作用主要有两个:共享数据和实现多态。Asio中用到继承的地方很多都只是共享数据,正如之前所说,虚函数会带来额外开销(overhead),在高性能的场景下应该避免使用虚函数。

继承会因为共享了过多不必要的数据而带来风险。一些现代的编程语言中没有继承,例如Rust语言:

Inheritance has recently fallen out of favor as a programming design solution in many programming languages because it’s often at risk of sharing more code than necessary. Subclasses shouldn’t always share all characteristics of their parent class but will do so with inheritance. This can make a program’s design less flexible. It also introduces the possibility of calling methods on subclasses that don’t make sense or that cause errors because the methods don’t apply to the subclass. In addition, some languages will only allow a subclass to inherit from one class, further restricting the flexibility of a program’s design.

For these reasons, Rust takes a different approach, using trait objects instead of inheritance.

参考

https://en.cppreference.com/w/cpp/language/constraints

http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-emulate

https://docs.cocos.com/creator/manual/zh/scripting/pooling.html#对象池的概念

https://doc.rust-lang.org/book/ch17-01-what-is-oo.html#inheritance-as-a-type-system-and-as-code-sharing

  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值