添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
帅气的蚂蚁  ·  Using Spline with ...·  6 天前    · 
苦闷的围巾  ·  const (C++) | ...·  5 天前    · 
着急的跑步鞋  ·  Fix 'Binding Element ...·  5 天前    · 
朝气蓬勃的黑框眼镜  ·  Hooks | Taro 文档·  5 天前    · 
淡定的大脸猫  ·  环球人物·  3 月前    · 
瘦瘦的马克杯  ·  is returning ...·  4 月前    · 
个性的小刀  ·  TypeReference' 为 ...·  6 月前    · 
inline bool compareSSE2(const char * p1, const char * p2)
    return 0xFFFF == _mm_movemask_epi8(              // 4) translate _m128i to int32
	_mm_cmpeq_epi8(                              // 3) 比较两个String
	        _mm_loadu_si128(reinterpret_cast<const __m128i *>(p1)),        // 1) 将String 1加载到寄存器
	        _mm_loadu_si128(reinterpret_cast<const __m128i *>(p2))));      // 2) 将String 2加载到寄存器

三、PodArray

PodArray是ClickHouse的自定义vector,ClickHouse中几乎所有的数据类型的列在内存中的表示都会用到PodArray,所以ClickHouse对PodArray也是进行了大量的优化。

绝大多数细致的优化都是针对场景的,所以首先明确PodArray设计的应用场景,它主要用于存储列式的数据,ClickHouse中列式的数据在内存中会划分为小的Chunk,默认Chunk的长度是6.5w左右,总结下来PodArray主要用于存储大量的数据的类似vector的数据结构。

1. 支持Stack内存分配

因为栈的空间是有限的,传统的动态数组结构比如std::vector,数据都是分配在堆上,这样的设计具有普适性,但是对数据的访问会有一次跳转,不利于数据cacheline的命中率。针对这一点ClickHouse提供了Stack上的内存分配方案PODArrayWithStackMemory

其工作原理如下。首先PodArray继承了AllocatorWithStackMemoryAllocatorWithStackMemory是一个内存分配器,其特点是当需要分配的内存小于一定阈值的时候使用栈上的空间,当大于阈值的时候分配对上的空间,并把栈上的数据拷贝到堆上。

2. Padding

PaddedPodArray是带有Padding的PodArray,其左右都填充了一些空白的内存空间,这些内存空间被初始化为了0,其内存结构如下:

    /// Size of i-th element, including terminating zero.
    size_t ALWAYS_INLINE sizeAt(ssize_t i) const 
         auto end_offset = i == 0 ? 0 : offsets[i - 1];
        return offsets[i] - end_offset; 

每次都需要判断i是不是为0,if语句会大大影响CPU指令的cache命中率,并且不能触发编译器的自动向量化操作,从而影响性能。

当有了left padding后,代码可以优化为:

    /// Size of i-th element, including terminating zero.
    size_t ALWAYS_INLINE sizeAt(ssize_t i) const 
        return offsets[i] - offsets[i - 1]; 

去掉了if,消除了对CPU指令cache命中率的影响,同时如果循环调用,可以触发编译器的自动向量化操作。

PS:CPU指令cache命中率对性能的影响可以参考:ClickHouse内幕(5)基于硬件的性能优化

PS:编译器自动向量化触发条件:requirements-for-vectorizable-loops

2.2 right padding

Right padding设计的主要作用是提升SIMD指令函数的效率并简化编码。比如:一个简单的SSE版本的memory copy函数,在没有right padding的情况下,其实现可能如下:

inline void memcpy(char * __restrict dst, const char * __restrict src, size_t n)
    auto aligned_n = n / 16 * 16;
    auto left = n - aligned_n;
    while (aligned_n > 0)
        _mm_storeu_si128(reinterpret_cast<__m128i *>(dst), 
            _mm_loadu_si128(reinterpret_cast<const __m128i *>(src)));
        dst += 16;
        src += 16;
        aligned_n -= 16;
    ::memcpy(dst, src, left);

但是如果dst和src各有15个byte的right padding,那么实现可以优化为如下:

inline void memcpy(char * __restrict dst, const char * __restrict src, size_t n)
    while (n > 0)
        _mm_storeu_si128(reinterpret_cast<__m128i *>(dst),
            _mm_loadu_si128(reinterpret_cast<const __m128i *>(src)));
        dst += 16;
        src += 16;
        n -= 16;
    // 这里无需额外处理结尾部分

3. emplace_back函数

PodArray的emplace_back函数,直接在末尾的内存空间上构建要插入的对象,相对提前构建然后在拷贝的方式减小了一次内存拷贝的开销。这个优化方式跟std::vector的emplace_back函数类似。

    template <typename... Args>
    void emplace_back(Args &&... args) /// NOLINT
        if (unlikely(this->c_end + sizeof(T) > this->c_end_of_storage))
            this->reserveForNextSize();
        new (t_end()) T(std::forward<Args>(args)...);   /// 在末尾构建对象,减少了数据拷贝的开销
        this->c_end += sizeof(T);

在创建对象的时候使用了placement new operator,其允许在指定内存地址创建对象,关于placement new operator请参考:stackoverflow