ClickHouse源码笔记3:函数调用的向量化实现

分享一下笔者研读ClickHouse源码时分析函数调用的实现,重点在于分析Clickhouse查询层实现的接口,以及Clickhouse是如何利用这些接口更好的实现向量化的。本文的源码分析基于ClickHouse v19.16.2.2的版本。

1.举个栗子

下面是一个简单的SQL语句
SELECT a, abs(b) FROM test

这里调用一个abs的函数,我们先打开ClickHouse的Debug日志看一下执行计划。(当前ClickHouse不支持使用Explain语句来查看执行计划,这个确实是很蛋疼的~~)

ClickHouse的执行PipeLine

这里分为了3个流

  • ExpressionBlockInputStream: 最顶层的Expression,实现了Projection,这个和我们今天主题无关,本质上就是实现一个简单列的改名操作。比如 select a as aaa from test这里将列名从a改为aaa.
  • ExpressionBlockInputStream: 第二个ExpressionBlockInputStream就是我们关注的重点的,后面的章节会详细的剖析它。它主要完成了下面两件事情
      1. b列执行函数abs,生成新的一列数据abs(b)
      1. remove column b, 将 b列删除。新的Block为a, abs(b)
  • TinyLogBlockInputStream: 存储引擎的读取流,这里标识了底层表的存储引擎为append onlyTinyLog

从上面的执行计划可以看出,Clickhouse的表达式计算是由ExpressionBlockInputStream来完成的,而这个类是一个很强大的类,可以实现:Projection, Join, Apply_Function, Add Column, Remove Column等。

2. 实现流程的梳理

  • ExpressionBlockInputSteam readImpl()的实现
    直接上代码,看一下ExpressionBlockInputStream的读取方法的实现
Block ExpressionBlockInputStream::readImpl()
{
    Block res = children.back()->read();
    if (res)
        expression->execute(res);
    return res;
}

这里的实现很简单,就是不停从底层的流读取数据Block,Block可以理解为Doris之中的Batch,相当一组数据,然后在Block之上执行表达式计算,之后返回给上节点。所以这里的重点就在于表达式计算的实现类ExpressionActions的指针expression,它封装了一组表达式的Action,在Block上依次执行这些Action

  • Action excute的实现
    Action支持多种操作,包含了:
enum Type {
        ADD_COLUMN,
        REMOVE_COLUMN,
        COPY_COLUMN,

        APPLY_FUNCTION,
        ARRAY_JOIN,
        JOIN,

        PROJECT,
        ADD_ALIASES,
    };

这里我们重点关注的是函数执行的实现,可以直接定位到APPLY_FUNCTION的代码:

case APPLY_FUNCTION:
        {
            1. 从Block之中筛选出对应的参数数组
            ColumnNumbers arguments(argument_names.size());
            for (size_t i = 0; i < argument_names.size(); ++i)
            {
                arguments[i] = block.getPositionByName(argument_names[i]);
            }
            
            2.新建一个结果的列,对应函数的结果会写入结果列,把结果列写入的Block之中
            size_t num_columns_without_result = block.columns();
            block.insert({ nullptr, result_type, result_name});
            
            3.调用对应的函数指针,执行函数调用
            function->execute(block, arguments, num_columns_without_result, input_rows_count, dry_run);

这里我保留一部分关键的执行路径代码,并添加了对应的中文注释。
选出了函数执行的参数,并添加了新的一个空列用于存储函数abs(b)的最终结果,新的列的偏移量就是num_columns_without_result指定的。

添加了新的一个空列

接下来这里我们这里重点关注Function的execute接口的参数就可以了:

  • block:实际存储的数据
  • arguments:列的参数偏移量
  • num_columns_without_result:函数计算结果的写入列
  • input_rows_count: block之中的数据行数

这里本质上是调用了接口IFunction的接口,它的子类需要实现对应的excuteImpl的方法:

class IFunction : public std::enable_shared_from_this<IFunction>,
                  public FunctionBuilderImpl, public IFunctionBase, public PreparedFunctionImpl
{
public:
    /// TODO: make const
    void executeImpl(Block & block, const ColumnNumbers & arguments, size_t result, size_t input_rows_count) override = 0;

而最终的实现是IFunction的子类:FunctionUnaryArithmetic实现了该方法,该方法的核心代码如下:

                if (auto col = checkAndGetColumn<ColumnVector<T0>>(block.getByPosition(arguments[0]).column.get()))
                {
                    auto col_res = ColumnVector<typename Op<T0>::ResultType>::create();
                    auto & vec_res = col_res->getData();
                    vec_res.resize(col->getData().size());
                    UnaryOperationImpl<T0, Op<T0>>::vector(col->getData(), vec_res);
                    block.getByPosition(result).column = std::move(col_res);
                    return true;
                }

这里最为核心的是,将arguments的列作为参数列取出为变量col, 而col_res创建了个新的列,存放result的结果。这里最重要的方法就是
UnaryOperationImpl<T0, Op<T0>>::vector,从名字上也能看出,它实现了函数的向量化计算,我们继续看这部分代码:

    static void NO_INLINE vector(const ArrayA & a, ArrayC & c)
    {
        size_t size = a.size();
        for (size_t i = 0; i < size; ++i)
            c[i] = Op::apply(a[i]);
    }

显然,这就是一个完美的向量化优化代码,没有任何if, switch, break的分支跳转语句,for循环的长度也是已知的。这里的Op::apply就是咱们调用的AbsImpl::apply函数的实现:

template <typename A>
struct AbsImpl
{
    static inline NO_SANITIZE_UNDEFINED ResultType apply(A a)
    {
        if constexpr (IsDecimalNumber<A>)
            return a < 0 ? A(-a) : a;
        else if constexpr (std::is_integral_v<A> && std::is_signed_v<A>)
            return a < 0 ? static_cast<ResultType>(~a) + 1 : a;
        else if constexpr (std::is_integral_v<A> && std::is_unsigned_v<A>)
            return static_cast<ResultType>(a);
        else if constexpr (std::is_floating_point_v<A>)
            return static_cast<ResultType>(std::abs(a));
    }

走的这里,相当于走完了整个函数调用的流程。而其他多参数的函数的实现也是大同小异,如:

struct BinaryOperationImplBase
{
    using ResultType = ResultType_;

    static void NO_INLINE vector_vector(const PaddedPODArray<A> & a, const PaddedPODArray<B> & b, PaddedPODArray<ResultType> & c)
    {
        size_t size = a.size();
        for (size_t i = 0; i < size; ++i)
            c[i] = Op::template apply<ResultType>(a[i], b[i]);
    }

而执行完成abs(b)函数之后,b列就没有用处了,Clickhouse会调用另一个Action:REMOVE_COLUM在Block之中删除b列,这样就得到了我们所需要的两个列a, abs(b)组成的新的Block。

计算的最终结果

3.要点梳理

第二小节梳理完成了一整个函数调用的流程,这里重点梳理一下实现向量化函数调要点:

  1. ClickHouse的计算是纯粹函数式编程式的计算,不会改变原先的列状态,而是产生一组新的列。
  2. 各个函数的实现需要继承IFunction的接口,实现execute 的方法,该方法基于Block进行执行。
  3. 最终继承IFunction接口的实现类都需要override的execute方法,并真正实现对应的函数vectoer的调用,这里Clickhouse确保了For循环的长度是已知的,同时没有对应跳转语句,确保了编译器进行向量化优化时有足够的亲和度。(这里可以打开gcc的编译flag:-fopt-info-vec或者clang的编译选项:-Rpass=loop-vectorize来查看实际源代码的向量化情况)

4. 小结

好了,到这里也就把ClickHouse函数调用的代码梳理完了。
除了abs函数外,其他的函数的执行也是同样通过类似的方式依次来实现和处理的,源码阅读的步骤也可以参照笔者的分析流程来参考。
笔者是一个ClickHouse的初学者,对ClickHouse有兴趣的同学,欢迎多多指教,交流。

5. 参考资料

官方文档
ClickHouse源代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 229,327评论 6 537
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,996评论 3 423
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 177,316评论 0 382
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,406评论 1 316
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,128评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,524评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,576评论 3 444
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,759评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,310评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,065评论 3 356
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,249评论 1 371
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,821评论 5 362
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,479评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,909评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,140评论 1 290
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,984评论 3 395
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,228评论 2 375

推荐阅读更多精彩内容