ClickHouse内幕(2)基础数据结构

news/2025/3/11 3:31:05/文章来源:https://www.cnblogs.com/Jcloud/p/18237243

ClickHouse以性能好被大家所熟知,而一个数据库的性能优化是一个庞大的系统性工程。本文着眼于ClickHouse内部的基础数据结构,以揭露ClickHouse性能优化的冰山一角。

在软件工程中并不是所有的执行路径都需要优化,只有关键执行路径才需要花费大力气进行优化。对于数据库领域来说关键执行路径,一句话就可以概括,一个查询中对每行数据都需要执行的函数或者代码。而基础数据类型是关键执行路径上的算法的基础,所以对它们的优化对性能有重要影响。

PS:本文的讨论基于ClickHouse 24.1。

最后更新于:2024-06-03

一、基础数据类型

根据实践经验有大概一半的列是字符串类型,所以字符串是一个重要基础数据类型。ClickHouse是一个列式数据库,数据处理的过程中也是以列式进行处理,每个列的数据需要用数组表示,所以数组也是重要数据类型。

本文讨论ClickHouse的内部的重要基础数据类型:StringRef、PodArray。

二、StringRef

StringRef的工作原理类似std:string_view,它可以表示对字符串序列的引用,比如字符串str=”abcdef”,那么StringRef(str.data() + 1, 2)表示”bc”,请注意这里StringRef实际上没有对”nc”进行拷贝。

StringRef被广泛运用在ClickHouse关键执行路径中,比如:内存中对于String类型列的表示ColumnString、Aggregate和Join算子用到的关键数据结构HashMap等。下图展示了String类型列中使用StringRef,避免了数据拷贝。

 

 

因为应用广泛,所以ClickHouse对StringRef进行了深度优化。

StringRef最重要的操作是判断相等,所以判断相等的函数memequalWide是重点优化对象。如何判断两个字符串相等,ClickHouse采用了批量处理的思路,而不是逐一对比每个字符。

1. size <= 16

当字符串长度小于等于16的时候,根据字符串的长度分成以下4中情况处理。主要思路是尽量将字符串当做比较大的数据类型(整型)做比较,以节约CPU计算周期。这么做的原因是64位CPU一次计算可以对比8个字符,将其当做较大的数据类型作比较可以最大限制的减小比较的次数。

由于不是所有情况size都能被数据类型的长度整除,对比前n个字节后,在再次对比后n个字节,这里是个很巧妙的设计,因为两次对比可以完成对所有字符的比较。

 

 

2. size > 16

 

 

1.对于大于64字节的部分使用compare64,compare64是4个compare8的组合。

2.对于剩余能被16整除的部分使用compare8,compare8是一个向量比较函数,使用SSE2指令集,一次对比两个16个字符的字符串。

3.使用compare8函数对比剩余部分的的后16个字符。

3. compare8函数

通过SSE2指令集(SIMD指令)一次性对比两个16个字符的数据是否相等。


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继承了AllocatorWithStackMemory,AllocatorWithStackMemory是一个内存分配器,其特点是当需要分配的内存小于一定阈值的时候使用栈上的空间,当大于阈值的时候分配对上的空间,并把栈上的数据拷贝到堆上。

2. Padding

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

 

 

2.1 left padding

ClickHouse中有很多变长的数据类型,变长的数据类型指的是每个值的长度是不固定的,比如String。对于这种数据结构在存储的时候需要存储一个offset数组和一个数据数组,如下:

 

 

当需要获取某个元素的时候,需要计算元素的长度,计算方式如下:


    /// 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的函数,直接在末尾的内存空间上构建要插入的对象,相对提前构建好对象然后在拷贝到PodArray的方式减小了一次内存拷贝的开销。这个优化方式跟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);
    }

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/721401.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PyQT5之菜单栏和工具栏

from PyQt5 import QtWidgets from PyQt5 import QtCore, QtGui import sys import cv2class ButtonPanel(QtWidgets.QWidget):def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)select_btn = QtWidgets.QPushButton("图像选择")self.path_lab…

CH32系列MCU SysTick使用与计算

1、关于SysTick CH32F103/203: CH32F103/203为Cortex-M3内核,SysTick是一个24位的向下递减计数器,计数器每计数一次的时间可配置为1/时基。当SysTick重装载数寄存器的值递减到0的时候,产生一次中断。CH32F系列MCU SysTick由4个寄存器控制,具体如下图。具体介绍可参考《CM3…

在线安装 qt 下载安装慢以及安装报错无法下载存档 not found——解决方式

一、下载安装QT的在线下载器可以在 QT 官网下载开源的安装包(需要登陆) 或者在各大大学的镜像站中下,比如:mirrors.nju.edu.cn(可选)解压出下载的压缩包,拿到 qt-unified-windows-x64-online.exe/dmg/run 本体在终端中,输入 ./包名 --mirror https://mirror.nju.edu.…

【Linux驱动设备开发详解】11.内存与I/O访问

1.内存管理单元 高性能处理器一般会提供一个内存管理单元(MMU),用于辅助操作系统尽心修改内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。 1.1MMU基本概念 1.1.1 概念含义 1.TLB(Translation Lookaside Buffer): 旁路转换缓存,TLB是MMU…

OOP第二次博客作业

一、前言 又做了三次PTA练习,前一次还是之前三次的迭代训练,后面两次又是一个新的模型。//终于是换模型了//题目类型都差不多,更注重类与类之间的联系,增加的内容就是对类的设计更复杂了,类的种类也更多了。但总体的逻辑不变。 二、分析第四次判题程序 (1)设计与分析 本…

HTTP Status 400 – Bad Request

1. 问题2. 原因org.apache.juli.logging.DirectJDKLog:log|Error parsing HTTP request headerNote: further occurrences of HTTP header parsing errors will be logged at DEBUG level.java.lang.IllegalArgumentException: Request header is too largeat org.apache.coyot…

使用jmeter,响应体response body中有两个同名的cookies时,如何获取第二个cookie进行跨线程组使用

如图两个同名cookie:.AspNetCore.Cookies正则表达式提取器 引用名称:loginCookie 正则表达式:Set-Cookie: (.AspNetCore.Cookies=.*?;) 模板:$1$(确保正确匹配到第二个 .AspNetCore.Cookies) 匹配数字2beanshell后置处理程序 ${__setProperty(loginCookie,${loginCookie…

IOS 手机 new Date 之后显示的是NaN-NaN-NaN

上周同事让我改一个入参,让用后端返回的时间作为入参,获取视频内容。我习惯成自然,利用了原来的时间格式化函数。函数里面有一个new Date()如下面截图: 部分IOS机型里面,2024-06-07里面的-他识别不出来,他会识别/,所以导致出现NaN-NaN-NaN。 这样的话,还是不要这样直接…

[中文参数] AGFA027R31C2I3V、AGFA027R31C2I3E、AGFA027R31C2E3E、AGFA027R31C2E4X面向互联世界的可编程逻辑产品

Agilex™ 7 F 系列采用Intel 10-nm SuperFin 工艺技术打造而成,提供高达 58 Gbps 的收发器速率、支持多种精度定点和浮点运算的高级 DSP 块,以及高性能的加密块。Agilex™ FPGA 产品组合包含一系列产品,可充分满足每一个技术领域(从边缘到嵌入式系统,再到通信和数据中心)…

美团面试:说说Netty的零拷贝技术?

零拷贝技术(Zero-Copy)是一个大家耳熟能详的技术名词了,它主要用于提升 IO(Input & Output)的传输性能。 那么问题来了,为什么零拷贝技术能提升 IO 性能? 1.零拷贝技术和性能 在传统的 IO 操作中,当我们需要读取并传输数据时,我们需要在用户态(用户空间)和内核态…

视频大模型 Vidu 支持音视频合成;字节跳动推出语音生成模型 Seed-TTS 丨 RTE 开发者日报 Vol.221

开发者朋友们大家好:这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「有看点的会议」,但内容仅代表编辑的个人观点…

基于 fastflow 的一种工作流框架

Why do we use "reconcile" in Cloud? 让我们思考下在云上为用户提供一种中间件服务,我们需要做什么?按照顺序编排申请各类云资源 —— 网络,S3,K8S,计算,存储 ……。 在 K8S 中自动化部署中间部署 完成各种初始化配置可以想象出看出在 Cloud 上为用户提供服务…