std::function 的性能陷进

前言

std::function 的作用很强大,他让vector等保存不同类型函数的对象: function, functor, lambda…成为了可能。但他有一个潜在的性能风险:在保存lambda/bind时,如果对象的大小 大于两个指针的大小,他就需要分配动态空间。这是大部分人写C++的人不期望看到的。

function什么分配内存

先看function实现的几段代码:6.1.0a-2/include/c++/6.1.0/functional

    void*       _M_access()       { return &_M_pod_data[0]; }
    const void* _M_access() const { return &_M_pod_data[0]; }

    template
      _Tp&
      _M_access()
      { return *static_cast<_Tp*>(_M_access()); }
   1651         _M_init_functor(_Any_data& __functor, _Functor&& __f, true_type)
   1652         { new (__functor._M_access()) _Functor(std::move(__f)); }
   1653
   1654         static void
   1655         _M_init_functor(_Any_data& __functor, _Functor&& __f, false_type)
   1656         { __functor._M_access<_Functor*>() = new _Functor(std::move(__f)); }

初始化的时候,重载了两个函数,决定是用_M_pod_data的空间作为存储空间,还是作为指针,指向分配的地址。
问题是,怎么决定调哪一个函数呢?看下面的代码:

   1625         static void
   1626         _M_init_functor(_Any_data& __functor, _Functor&& __f)
   1627         { _M_init_functor(__functor, std::move(__f), _Local_storage()); }

调用哪个函数由_Local_storage决定.

   1538   class _Function_base
   1539   {
   1540   public:
   1541     static const std::size_t _M_max_size = sizeof(_Nocopy_types);
   1542     static const std::size_t _M_max_align = __alignof__(_Nocopy_types);
   1543
   1544     template
   1545       class _Base_manager
   1546       {
   1547       protected:
   1548         static const bool __stored_locally =
   1549         (__is_location_invariant<_Functor>::value
   1550          && sizeof(_Functor) <= _M_max_size
   1551          && __alignof__(_Functor) <= _M_max_align
   1552          && (_M_max_align % __alignof__(_Functor) == 0));
   1553
   1554         typedef integral_constant _Local_storage;

local storage的true或者false由__stored_locally决定。__stored_locally的大小决定中有一条:sizeof(_Functor) <= _M_max_size;关键看Functor的大小是否小于_M_max_size,_M_max_size的大小等于_Nocopy_types。接着看_Nocopy_types的实现:

   1457   union _Nocopy_types
   1458   {
   1459     void*       _M_object;
   1460     const void* _M_const_object;
   1461     void (*_M_function_pointer)();
   1462     void (_Undefined_class::*_M_member_pointer)();
   1463   };

咋一看,这个union的大小应该是一个指针的大小,里面有个关键成员:void (_Undefined_class:?_M_member_pointer)(); 指向成员函数的指针,指向成员函数的指针的大小是2个指针的大小。所以结果就是:如果传入functor的大小大于两个指针的大小,就会分配动态内存。函数指针的大小就是一个指针的大小,那什么时候functor会大于一个指针的大小:lambda和bind。我们以lambda为例来看。bind和lambda的效果一样。

lambda函数的大小

编译器在碰到lambda函数时,事实上,会生成一个对象来保存lambda函数。这个对象重载了operator(),所以能像函数一样调用。同时,这个对象还有数据成员,这些数据成员就是捕获来的对象。譬如:

 auto lf = [&a, &b]{ return 0;};
 std::cout << sizeof(lf) << std::endl;
``
这个对象捕获了两个引用(相当于两个指针的大小),所以输出就是16.
# 类成员函数的指针的大小
前面提到类成员函数的指针大小是两个指针的大小。为什么? 因为类成员函数的指针还有一个偏移量, 这个类相对于基类指针的偏移 。
考虑三个类:
```C++
#include 
#include 

class A{
public:
    int getA() {
        std::cout << "A address: " << this << "\n";
        return a;
    }
private:
    int a{1};
};
class B{
public:
    int getB() {
        std::cout << "B address: " << this << "\n";
        return a;
    }

private:
    int a{2};
};

class Derived: public A, public B {
};

我们用Derived的对象去访问函数getA期望返回的是class A的a, 如果访问getB期望返回的是class B的a;

    Derived d;
    std::cout << "getA: " << d.getA() << ", getB: " << d.getB() << std::endl;

输出是:
B address: 0x7ffe00762494
A address: 0x7ffe00762490
getA: 1, getB: 2
访问B的时候,this指针做了偏移,指向了class B在class D中的开始地址。假如使用成员函数指针,这个偏移量,只有在成员函数指针被赋值的时候才能知道。
考虑函数:

void print(Derived & d,  int (Derived::*p_mem)())
{
    void *data[2];
    std::memcpy(data, &p_mem, sizeof(p_mem));
    std::cout << "print --------\n"
              << "object ptr: " << &d << "\n"
              << "function ptr: " << data[0] << "\n"
              << "point adj: " << data[1] << "\n"
              << "value: " << (d.*p_mem)() << std::endl;
}

print(d, &Derived::getA);
print(d, &Derived::getB);
的输出就是:
A address: 0x7ffc8164e770
print --------
object ptr: 0x7ffc8164e770
function ptr: 0x400b82
point adj: 0
value: 1
B address: 0x7ffc8164e774
print --------
object ptr: 0x7ffc8164e770
function ptr: 0x400bc4
point adj: 0x4
value: 2

参考文章:http://lazarenko.me/wide-pointers/

总结

如果用std::function的变量来保存lambda或bind的函数对象时,需要格外注意,如果对象的大小大于两个指针的大小,就会发生动态内存分配,影响性能。

你可能感兴趣的:(C++)