本地函数定义是非法的_Hands-On Design Patterns With C++(十)本地缓存优化(下)

709b38449f0f3f887a51e21103d136a2.png

目录:

trick:Hands-On Design Patterns With C++(零)前言​zhuanlan.zhihu.com
3556d8b269a5811ef0498db2b0096bf6.png

本地缓存优化(上)

trick:Hands-On Design Patterns With C++(十)本地缓存优化(上)​zhuanlan.zhihu.com
3556d8b269a5811ef0498db2b0096bf6.png

本地缓存优化(下)

本地缓存优化策略不仅仅用在短字符串这样的场景。事实上,运行时动态的小型内存分配都可以考虑使用此方法进行优化。本文,我们要讨论其它使用本地缓存优化的案例以及本地缓存优化的缺点。

其它使用本地缓存优化的案例

(一)自定义小型vector

vector是指定类型的动态连续数组。C++中的std::vector需要两个成员变量:数据指针与元素个数。下面我们按照simple_string与small_string的方式,完成第一版vector:

class simple_vector {
public:
    simple_vector() : n_(), p_() {}
    simple_vector(std::initializer_list<int> il) : n_(il.size()), p_(static_cast<int *>(malloc(sizeof(int) * n_))) { // 初始化构造函数,malloc
        int *p = p_;
        for (auto x : il) {
            *p++ = x;
        }
    }
    ~simple_vector() {
        free(p_);
    }
    size_t size() const { return n_; }
private:
    size_t n_; // 元素个数
    int *p_; // 数据指针
};

class small_vector { // 第一版vector
public:
    small_vector() : n_(), p_() {}
    small_vector(std::initializer_list<int> il) : 
                n_(il.size()),  // 初始化大小
               p_(n_ < sizeof(buf_) / sizeof(buf_[0]) ? buf_ : static_cast<int *>(malloc(sizeof(int) * n_))) // 选择使用哪种方式申请内存
    {
        int *p = p_;
        for (auto x : il) *p++ = x;
    }

    ~small_vector() {
        if (p_ != buf_) free(p_);
    }

private:
    size_t n_; // 元素个数
    int *p_; // 大于buf_内存时使用
    int buf_[16]; // vecotr小于此大小使用
};

我们没有使用与small_string一样的tag作为是否使用本地缓存的标志位,我们有n_表示元素个数,我们可以根据元素个数判断是否使用本地缓存。我们对small_vector更新一下,将:

class small_vector {
public:
    small_vector() { short_.n = 0; }

    small_vector(std::initializer_list<int> il) {
        int *p;
        if (il.size() < sizeof(short_.buf) / sizeof(short_.buf[0])) { // 判断长度,< 15时用本地缓存
            short_.n = il.size();
            p = short_.buf;
        } else { // 判断长度,else中使用malloc
            short_.n = UCHAR_MAX; // short_.n设置为默认值
            long_.n = il.size();
            p = long_.p = static_cast<int *>(malloc(sizeof(int) * long_.n));
        }
        for (auto x : il) *p++ = x;
    }

    ~small_vector() {
        if (short_.n == UCHAR_MAX) free(long_.p);
    }

private:
    union {
        struct {
            size_t n; // 长度较长,使用size_t表示长度
            int *p;
        } long_; // 定义长结构体类型,用malloc
        struct {
            int buf[15];
            unsigned char n; // 使用unsigned char表示缓存长度(最长可表示255)
        } short_; // 定义短结构体类型,用本地缓存
    };
};

我们将long vector和small vector分别包装成结构体,根据长度条件判断是否使用本地缓存,我们通过benchmark测试性能:

template<typename T>
void BM_vector_create_short(benchmark::State &state) {
    std::initializer_list<int> il{2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
    for (auto _ : state) {
        REPEAT({
                   T v(il);
                   benchmark::DoNotOptimize(v);
               })
    }
    state.SetItemsProcessed(32 * state.iterations());
}

template<typename T>
void BM_vector_create_long(benchmark::State &state) {
    std::initializer_list<int> il{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79,
                                  83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
                                  179, 181, 191, 193, 197, 199};
    for (auto _ : state) {
        REPEAT({
                   T v(il);
                   benchmark::DoNotOptimize(v);
               })
    }
    state.SetItemsProcessed(32 * state.iterations());
}

BENCHMARK_TEMPLATE1(BM_vector_create_short, simple_vector);
BENCHMARK_TEMPLATE1(BM_vector_create_short, small_vector);
BENCHMARK_TEMPLATE1(BM_vector_create_long, simple_vector);
BENCHMARK_TEMPLATE1(BM_vector_create_long, small_vector);

BENCHMARK_MAIN();

经过测试,局部缓存版本性能是普通版本的十倍以上。当然,其它数据结构进行动态内存分配时,也可以用相同的方式进行优化。

(二)类型擦除与回调对象

存储回调对象也可以使用本地缓存优化策略。需对模板类提供了一个选项,使用回调对象完成某些工作,例如std::shared_ptr,用户可以传入一个自定义deleter回调函数表示内存的释放方式。用法如下:

class Sample {}
void deleter(Sample *x) {
    std::cout<<"DELETE FUNCTION CALLEDn";
    delete[] x;
}
std::shared_ptr<Sample> p3(new Sample[12], deleter);

shared_ptr第二个参数是void*类型,可以是方法指针,成员函数指针或一个方法对象(包含operator()定义的对象)。

知识回顾:第六章类型擦除。unique_ptr的Deleter是模板类型参数,而shared_ptr的Deleter不是。因为shared_ptr声明必须是完全一样的类型,所以构造函数中声明Deleter,后面不用也不允许再传入Deleter了。
注意:unique_ptr与shared_ptr都必须存储一个deleter对象,只是shared_ptr对deleter对象进行了类型擦除(void *),unique_ptr将至放到了模板参数中。
参考:
第六章类型擦除: https:// zhuanlan.zhihu.com/p/12 5168286
为什么unique_ptr模板中有Deleter,而shared_ptr没有: https://www. cnblogs.com/fuzhe1989/p /7763623.html

deleter必须被存储在某处,共享指针需要可以获取到deleter。deleter是共享指针的一部分,我们需要在共享指针对象中声明该类型的数据成员。

您可以将其视作本地缓存优化的一个简单应用,下面我们实现一个自定义智能指针,在最后会自动释放资源

// 一般的智能指针,没有进行类型擦除。
template<typename T, typename Deleter>
class smartptr {
public:
    smartptr(T *p, Deleter d) : p_(p), d_(d) {} 
    ~smartptr() { d_(p_); }
    T *operator->() { return p_; }
    const T *operator->() const { return p_; }
private:
    T *p_;
    Deleter d_; // 删除器
};

回调存储在一个多态对象中,虚函数在运行其调用正确的版本。相反,多态对象通过基类指针进行操作。

现在,我们的问题是,我们需要存储一些小型数据,在我们的例子中,可调用的对象、其类型与大小是不可知的。一般的解决方案是动态分配这些对象,并通过基类指针访问它们。我们可以这样做:

template<typename T>
class smartptr_te { // te - type erase 类型擦除
    struct deleter_base { // deleter基类
        virtual void apply(void *) = 0; // 纯虚函数
        virtual ~deleter_base() {} // 虚析构
    };

    template<typename Deleter>
    struct deleter : public deleter_base { // deleter派生类
        deleter(Deleter d) : d_(d) {}
        virtual void apply(void *p) { d_(static_cast<T *>(p)); }
        Deleter d_;
    };

public:
    template<typename Deleter> // Deleter为模板类型
    smartptr_te(T *p, Deleter d) : p_(p), d_(new deleter<Deleter>(d)) {} // Deleter是deleter类的模板参数,deleter_base是smartptr_te的类型,所以Deleter不再是smartptr_te的类型,类型被擦除了。

    ~smartptr_te() {
        d_->apply(p_);
        delete d_;
    }

    T *operator->() { return p_; }

    const T *operator->() const { return p_; }

private:
    T *p_; // 存储的数据指针
    deleter_base *d_; // 删除器
};

注意,Deleter模板类型不再是smartptr_te的删除器类型了,它被擦除了(见代码注释)。对于同一类型T,它的所有智能指针只有一种类型smartptr_te<T>。但是,我们为了这种类型擦除语法的便利性,牺牲了性能:每次创建智能指针时,都会额外分配内存。

性能测试仍然通过benchmark测出:

struct deleter { // 一个简单的deleter对象
    template<typename T>
    void operator()(T *p) { delete p; } // 具有operator()方法,删除是deleter,所以new出来的对象用于此
};

void BM_smartptr(benchmark::State &state) {
    deleter d;
    for (auto _ : state) {
        smartptr<int, deleter> p(new int, d);
    }
    state.SetItemsProcessed(state.iterations());
}

void BM_smartptr_te(benchmark::State &state) {
    deleter d;
    for (auto _ : state) {
        smartptr_te<int> p(new int, d);
    }
    state.SetItemsProcessed(state.iterations());
}

// 跑两种测试
BENCHMARK(BM_smartptr);
BENCHMARK(BM_smartptr_te);

BENCHMARK_MAIN();

性能损耗如下:

1fb2435fe5a9c9122d75dbc84dea6a8a.png

可以看出,BM_smartptr_te类型擦除的智能指针是普通指针的两倍性能损耗。

我们既想要保留类型擦除,又想保证性能,怎么办呢?这时候我们就可以是哟给你本地缓存优化的技巧了,我们需要一个本地缓存成员变量:

template<typename T>
class smartptr_te_lb { // lb - local buffer
    struct deleter_base {
        virtual void apply(void *) = 0;
        virtual ~deleter_base() {}
    };

    template<typename Deleter>
    struct deleter : public deleter_base {
        deleter(Deleter d) : d_(d) {}
        virtual void apply(void *p) { d_(static_cast<T *>(p)); }
        Deleter d_;
    };

public:
    template<typename Deleter>
    smartptr_te_lb(T *p, Deleter d) :
            p_(p),
            d_(sizeof(Deleter) > sizeof(buf_) ? new deleter<Deleter>(d) : new(buf_) deleter<Deleter>(d)) {} // 根据deleter的大小选择是否使用本地缓存

    ~smartptr_te_lb() {
        d_->apply(p_);
        if (static_cast<void *>(d_) == static_cast<void *>(buf_)) {
            d_->~deleter_base(); // 本地缓存释放
        } else {
            delete d_; // 普通释放方式
        }
    }

    T *operator->() { return p_; }

    const T *operator->() const { return p_; }

private:
    T *p_;
    deleter_base *d_;
    char buf_[16]; // 一个本地buffer成员变量
};

我们仍然增加一个本地缓存成员变量char buf_[16],构造函数deleter的大小当作tag表示是否使用缓存变量。

通过benchmark测试如下:

void BM_smartptr_te_lb(benchmark::State &state) {
    deleter d;
    for (auto _ : state) {
        smartptr_te_lb<int> p(new int, d);
    }
    state.SetItemsProcessed(state.iterations());
}

BENCHMARK(BM_smartptr_te_lb);
BENCHMARK_MAIN();

最终结果是,smartptr耗时21ns,普通类型擦除智能指针smartptr_te耗时42ns,而带有本地缓存的类型擦出智能指针smartptr_te_lb耗时23ns,性能提升明显!相比与smartptr,smartptr_te_lb多出来的一点点耗时来源于检查是否使用本地缓存上。

本地缓存优化的缺点

缺点一:对象大小

本地缓存优化并非没有缺点。一个显而易见的缺点就是:具有本地缓存的对象大小一般都大于没有本地缓存的对象。如果缓存的数据大小一直小于对象中缓存成员变量的大小,那么剩余的这部分内存就是一个资源的浪费。

极端情况是,如果传入的数据大于本地缓存,那么本地缓存变量就不会被使用,那么这部分内存就会整块被浪费。所以我们应该衡量有效的数据大小范围,选择一个适合的缓存大小,以空间换取性能上的平衡。

缺点二:存储位置变动

另一个复杂的地方在于:以前是对象外部的数据,现在存储于对象内部。首先,它不适用于计数引用的设计,例如一个写入时复制(Copy-On-Write COW)的字符串就无法使用small string这样的优化。

其次,如果对象本身被移动,数据也必须被移动。与std::vector比较,移动与交换操作通过移动指针便可,数据保持不变。你可能认为这没什么,本地缓存很小,移动他们所消耗的成本与复制指针的成本相当。然而,比性能更重要的地方:移动std::vector的实例不会发生异常(仅仅交换指针)。而移动数据本身可能会产生异常。因此,只有在包含对象为std::is_nothrow_move_constructible时,才能使用本地缓存优化。

vector要求移动、交换操作不能让任何迭代器指针(iterator)失效。显然,此要求不适合于本地缓存优化,因为移动一个small vector将要重定位到另一块不同的内存地址。

因此,许多性能库都提供了一个自定义的vector-like的容器,他们支持small vector优化,但是付出的代价就是迭代器(iterator)失效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值