分享一下笔者研读ClickHouse源码时分析函数调用的实现,重点在于分析Clickhouse查询层实现的接口,以及Clickhouse是如何利用这些接口更好的实现向量化的。本文的源码分析基于ClickHouse v19.16.2.2的版本。
1.举个栗子
下面是一个简单的SQL语句SELECT a, abs(b) FROM test
这里调用一个abs的函数,我们先打开ClickHouse的Debug日志看一下执行计划。(当前ClickHouse不支持使用Explain语句来查看执行计划,这个确实是很蛋疼的~~)
这里分为了3个流
- ExpressionBlockInputStream: 最顶层的Expression,实现了Projection,这个和我们今天主题无关,本质上就是实现一个简单列的改名操作。比如
select a as aaa from test
这里将列名从a
改为aaa
. - ExpressionBlockInputStream: 第二个ExpressionBlockInputStream就是我们关注的重点的,后面的章节会详细的剖析它。它主要完成了下面两件事情
- 对
b
列执行函数abs
,生成新的一列数据abs(b)
- 对
remove column b
, 将b
列删除。新的Block为a, abs(b)
- TinyLogBlockInputStream: 存储引擎的读取流,这里标识了底层表的存储引擎为
append only
的TinyLog
。
从上面的执行计划可以看出,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.要点梳理
第二小节梳理完成了一整个函数调用的流程,这里重点梳理一下实现向量化函数调要点:
- ClickHouse的计算是纯粹函数式编程式的计算,不会改变原先的列状态,而是产生一组新的列。
- 各个函数的实现需要继承IFunction的接口,实现
execute
的方法,该方法基于Block进行执行。 - 最终继承IFunction接口的实现类都需要override的
execute
方法,并真正实现对应的函数vectoer
的调用,这里Clickhouse确保了For循环的长度是已知的,同时没有对应跳转语句,确保了编译器进行向量化优化时有足够的亲和度。(这里可以打开gcc的编译flag:-fopt-info-vec
或者clang的编译选项:-Rpass=loop-vectorize
来查看实际源代码的向量化情况)
4. 小结
好了,到这里也就把ClickHouse函数调用的代码梳理完了。
除了abs函数外,其他的函数的执行也是同样通过类似的方式依次来实现和处理的,源码阅读的步骤也可以参照笔者的分析流程来参考。
笔者是一个ClickHouse的初学者,对ClickHouse有兴趣的同学,欢迎多多指教,交流。
5. 参考资料
官方文档
ClickHouse源代码
原文转载:http://www.shaoqun.com/a/576612.html
急速:https://www.ikjzd.com/w/1861
赛兔:https://www.ikjzd.com/w/2375
分享一下笔者研读ClickHouse源码时分析函数调用的实现,重点在于分析Clickhouse查询层实现的接口,以及Clickhouse是如何利用这些接口更好的实现向量化的。本文的源码分析基于ClickHousev19.16.2.2的版本。1.举个栗子下面是一个简单的SQL语句SELECTa,abs(b)FROMtest这里调用一个abs的函数,我们先打开ClickHouse的Debug日志看一下
auditor:https://www.ikjzd.com/w/2437
递四方:https://www.ikjzd.com/w/1066
prime day:https://www.ikjzd.com/w/131.html
选品:亚马逊3C消费电子产品海外消费者画像:https://www.ikjzd.com/home/125739
拼图、空气炸锅、筋膜枪、环形灯...这些热卖产品你选中了几个:https://www.ikjzd.com/home/139699
这五个方法帮你减少亚马逊广告预算:https://www.ikjzd.com/home/133462
No comments:
Post a Comment