Doris向量化引擎部分实现分析

数据结构分析

Block与RowBatch

doris算子之间数据流的传递单位Block-Column,是在原有Tuple-RowBatch数据结构的基础上改进而来的,两者的关系大致为下图所示:
在这里插入图片描述
总体来看,Block和RowBatch存储的都是数据的一部分,但两者设计的维度不同。Block以Column作为单位,按列来存储若干行的数据,简单理解就是把Impala中的Tuple变为Column,并把多个Column组装为一个Block,以列的方式来管理;RowBatch是以Tuple作为单位,把不同列的同一行数据组装为Tuple,每列的单个数据是不同的Slot,然后把多个Tuple组装为RowBatch。这样的好处是,类似聚合这种整个字段的计算可以直接在获取到Block后对列批量进行,而无需遍历RowBatch中的每个Slot进行计算,两者虽都需要进行一定程度的遍历,Column计算可以比较好地利用Cache,还可以通过SIMD指令来加速Column计算,而RowBatch的计算操作需要不停地分别遍历RowBatch中的行和Slot。

Block相关实现:

class Block {
private:
    // ColumnsWithTypeAndName是std::vector<ColumnWithTypeAndName>的别名
    using Container = ColumnsWithTypeAndName;
    using IndexByName = phmap::flat_hash_map<String, size_t>;

    Container data;
    IndexByName index_by_name;
    std::vector<bool> row_same_bit;
    
    // ...一些记录性能的指标metric声明...
public:
    void some_functions();
}

ColumnWithTypeAndName相关实现:

struct ColumnWithTypeAndName {
    // 存储各个Column的数据
    ColumnPtr column;
    // 存储Column数据类型
    DataTypePtr type;
    // Column名称
    String name;

    void some_functions();
};

从实现上能看出,Block有三个主要成员:指向Column数据的指针、Column的数据类型指针、列名,Column指针基于Copy-On-Write模式,可以降低数据拷贝的次数,在多读少写的场景能够节约内存并提高效率。

COW(Copy-On-Write)

Doris中的基础数据结构Block使用了COW的思想,简单解释就是同一份数据有多个副本时,实际使用的是相同地址,一旦有哪个线程或对象对这部分数据进行了写操作,就将其变为独立的副本,从原始地址拷贝出来,这种实现对于多读少写的数据结构可以有比较大的内存上的优化,其实现略为复杂:

template <typename Derived>
class COW {
    std::atomic_uint ref_counter;

protected:
    // ...一些构造函数实现...
    
    // 引用计数+1
    void add_ref() { ++ref_counter; }

    // 释放指针时引用计数-1
    void release_ref() {
        if (--ref_counter == 0) {
            delete static_cast<const Derived*>(this);
        }
    }

    Derived* derived() { return static_cast<Derived*>(this); }

    const Derived* derived() const { return static_cast<const Derived*>(this); }

    // COW的内部类,其构造函数中实现了引用计数,其子类用于外部使用
    template <typename T>
    class intrusive_ptr {
    public:
        intrusive_ptr() : t(nullptr) {}
        // 构造时自动增加引用计数
        intrusive_ptr(T* t, bool add_ref = true) : t(t) {
            if (t && add_ref) {
                ((std::remove_const_t<T>*)t)->add_ref();
            }
        }
        // 构造时自动增加引用计数
        template <typename U>
        intrusive_ptr(intrusive_ptr<U> const& rhs) : t(rhs.get()) {
            if (t) {
                ((std::remove_const_t<T>*)t)->add_ref();
            }
        }
        // 构造时自动增加引用计数
        intrusive_ptr(intrusive_ptr const& rhs) : t(rhs.get()) {
            if (t) {
                ((std::remove_const_t<T>*)t)->add_ref();
            }
        }
        // 析构时自动减少引用计数
        ~intrusive_ptr() {
            if (t) {
                ((std::remove_const_t<T>*)t)->release_ref();
            }
        }
        // ...一些其他构造函数实现...
        // ...一些常用运算符重载实现...

        template <class U>
        friend class intrusive_ptr;

        // ...一些getter、指针相关常用成员函数实现...

    private:
        T* t;
    };

protected:
    // 针对可变对象使用
    template <typename T>
    class mutable_ptr : public intrusive_ptr<T> {
    private:
        // 可变指针
        using Base = intrusive_ptr<T>;

        template <typename>
        friend class COW;
        template <typename, typename>
        friend class COWHelper;

        explicit mutable_ptr(T* ptr) : Base(ptr) {}

    public:
        // 不允许拷贝构造
        mutable_ptr(const mutable_ptr&) = delete;

        /// 允许移动构造
        mutable_ptr(mutable_ptr&&) = default;
        mutable_ptr& operator=(mutable_ptr&&) = default;

        // ...其他构造函数实现...
    };

public:
    using MutablePtr = mutable_ptr<Derived>;

    unsigned int use_count() const { return ref_counter.load(); }

protected:
    // 针对不可变对象使用
    template <typename T>
    class immutable_ptr : public intrusive_ptr<const T> {
    private:
        // 不可变指针
        using Base = intrusive_ptr<const T>;

        template <typename>
        friend class COW;
        template <typename, typename>
        friend class COWHelper;
        // 禁止隐式类型转换
        explicit immutable_ptr(const T* ptr) : Base(ptr) {}

    public:
        // 允许拷贝构造
        immutable_ptr(const immutable_ptr&) = default;
        immutable_ptr& operator=(const immutable_ptr&) = default;

        // 允许移动构造
        immutable_ptr(immutable_ptr&&) = default;
        immutable_ptr& operator=(immutable_ptr&&) = default;

        // 允许从可变指针移动构造
        template <typename U>
        immutable_ptr(mutable_ptr<U>&& other) : Base(std::move(other)) {}

        // 不允许从可变指针拷贝构造
        template <typename U>
        immutable_ptr(const mutable_ptr<U>&) = delete;
        
        // ...其他构造函数实现...
    };

public:
    using Ptr = immutable_ptr<Derived>;

    template <typename... Args>
    static MutablePtr create(Args&&... args) {
        return MutablePtr(new Derived(std::forward<Args>(args)...));
    }

    template <typename T>
    static MutablePtr create(std::initializer_list<T>&& arg) {
        return create(std::forward<std::initializer_list<T>>(arg));
    }

public:
    Ptr get_ptr() const { return Ptr(derived()); }
    MutablePtr get_ptr() { return MutablePtr(derived()); }

protected:
    // 通过mutate()调用
    MutablePtr shallow_mutate() const {
        // 如果引用计数大于1,就创建新对象,并返回其指针
        if (this->use_count() > 1) {
            return derived()->clone();
        } else {
        // 没有其他对象在引用,可直接返回其副本
            return assume_mutable();
        }
    }

public:
    // 尝试对当前对象进行修改
    MutablePtr mutate() const&& { return shallow_mutate(); }
    // 将当前对象直接进行修改,即看作是无对象正在引用
    MutablePtr assume_mutable() const { return const_cast<COW*>(this)->get_ptr(); }
    // 返回引用,也是直接修改当前对象
    Derived& assume_mutable_ref() const { return const_cast<Derived&>(*derived()); }

protected:
    // 可使用在不同上下文场景,const场景下作为immutable_ptr使用,非const场景通过assume_mutable_ref直接返回可变对象引用
    template <typename T>
    class chameleon_ptr {
    private:
        immutable_ptr<T> value;

    public:
        template <typename... Args>
        chameleon_ptr(Args&&... args) : value(std::forward<Args>(args)...) {}

        template <typename U>
        chameleon_ptr(std::initializer_list<U>&& arg)
                : value(std::forward<std::initializer_list<U>>(arg)) {}

        const T* get() const { return value.get(); }
        T* get() { return &value->assume_mutable_ref(); }

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

        const T& operator*() const { return *value; }
        T& operator*() { return value->assume_mutable_ref(); }

        operator const immutable_ptr<T>&() const { return value; }
        operator immutable_ptr<T>&() { return value; }

        operator bool() const { return value != nullptr; }
        bool operator!() const { return value == nullptr; }

        bool operator==(const chameleon_ptr& rhs) const { return value == rhs.value; }
        bool operator!=(const chameleon_ptr& rhs) const { return value != rhs.value; }
    };

public:
    using WrappedPtr = chameleon_ptr<Derived>;
};

// COW类的helper类,主要用于实现copy时clone
template <typename Base, typename Derived>
class COWHelper : public Base {
public:
    // Ptr表示不可变对象指针
    using Ptr = typename Base::template immutable_ptr<Derived>;
    // MutablePtr表示可变对象指针
    using MutablePtr = typename Base::template mutable_ptr<Derived>;

    template <typename... Args>
    static MutablePtr create(Args&&... args) {
        return MutablePtr(new Derived(std::forward<Args>(args)...));
    }

    typename Base::MutablePtr clone() const override {
        return typename Base::MutablePtr(new Derived(static_cast<const Derived&>(*this)));
    }

protected:
    MutablePtr shallow_mutate() const {
        return MutablePtr(static_cast<Derived*>(Base::shallow_mutate().get()));
    }
};

COW特性的实现大概分为四个部分,COW基类、具体实现了指针功能的intrusive_ptr内部类、继承了intrusive_ptr的可变对象指针mutable_ptr和不可变对象指针immutable_ptr、继承了COW类的COWHelper类以及chameleon_ptr类。COW基类仅实现了基础的引用计数,具体的COW功能由三个内部类intrusive_ptr、mutable_ptr和immutable_ptr实现,intrusive_ptr中实现了一些基础的指针功能,它的两个子类mutable_ptr和immutable_ptr分别实现了可变对象和不可变对象的特性,具体来说,两个子类中分别封装了T类型和const T类型的指针,通过COWHelper类进行不同的const上下文场景分配。在实际使用过程中,继承了COW类后,通过mutate()函数获取当前对象,根据引用计数来判断是否创建另外的副本。

聚合过程分析

聚合相关操作实现

Doris在聚合计算过程中使用了一种比较灵活的方式,事先声明了一个executor结构体,其中封装了多个std::function,分别代表执行阶段可能需要调用到的函数,在prepare阶段会使用std::bind将函数绑定到具体的实现上,根据是否开启streaming pre-agg、是否存在group by、是否存在distinct等条件来确定具体绑定什么函数。

struct executor {
    vectorized_execute execute;
    vectorized_pre_agg pre_agg;
    vectorized_get_result get_result;
    vectorized_closer close;
    vectorized_update_memusage update_memusage;
};

这几个函数的大致调用关系过程可如下所示:
在这里插入图片描述
对应的,相关绑定过程:
在这里插入图片描述
下面选取几个函数进行分析。

有group by、不需要Finalize的聚合场景如distinct、非最终阶段的聚合计算时,get_result获取结果使用的函数是_get_with_serialized_key_result,下面简化其实现进行分析:

Status AggregationNode::_get_with_serialized_key_result(RuntimeState* state, Block* block,
                                                        bool* eos) {
    MutableColumns key_columns;
    MutableColumns value_columns;
    //...进行一些初始化...
    std::visit(
            [&](auto&& agg_method) -> void {
                //...进行一些初始化...                
                while (iter != _aggregate_data_container->end() &&
                       num_rows < state->batch_size()) {
                    keys[num_rows] = iter.get_key<KeyType>();
                    _values[num_rows] = iter.get_aggregate_data();
                    ++iter;
                    ++num_rows;
                }
                // 一次性将所有group by分组列插入目标Column中
                agg_method.insert_keys_into_columns(keys, key_columns, num_rows, _probe_key_sz);                
                // 根据不同的聚合函数(SUM、AVG、COUNT等等)批量将聚合操作的结果(非group by分组列)插入目标Column中
                for (size_t i = 0; i < _aggregate_evaluators.size(); ++i) {
                    _aggregate_evaluators[i]->insert_result_info_vec(
                            _values, _offsets_of_aggregate_states[i], value_columns[i].get(),
                            num_rows);
                }
                //...NULL值处理...                
            },
            _agg_data->_aggregated_method_variant);
    //...将聚合结果和group by分组列组装为最终结果...
    return Status::OK();
}

_get_with_serialized_key_result的实现总体大概分为三部分,首先将所有的聚合数据取出来,数据按照聚合列和分组列分为key_columns和values_columns,均通过std::vector存储在_aggregate_data_container中,这个数据结构提供了类似std::vector的读写方式,可以进行弹性扩缩容。数据取出后,通过insert_keys_into_columns将分组列放入结果Column中,这个过程经过了类似向量化的实现改造,以String类型的分组列为例,keys是连续内存分布的数组,将其插入到key_columns是直接通过memcpy一次性进行的,这部分逻辑在优化前通过iter每取出一行数据,就进行一次分组列插入,相当于会多执行最多batch_size次插入。insert_result_info_vec也使用了类似的优化方式,按照不同的聚合函数进行value_columns的填充,两个column处理结束后,将其合并为一个完整的Block,作为这个AGG_NODE的输出。

启用streaming pre-agg时,会进行预聚合,流式预聚合相关实现:

Status AggregationNode::_pre_agg_with_serialized_key(doris::vectorized::Block* in_block,doris::vectorized::Block* out_block) {
    // ...一些初始化...
    // 获取key_column
    for (size_t i = 0; i < key_size; ++i) {
        int result_column_id = -1;
        RETURN_IF_ERROR(_probe_expr_ctxs[i]->execute(in_block, &result_column_id));
        in_block->get_by_position(result_column_id).column = in_block->get_by_position(result_column_id).column->convert_to_full_column_if_const();
        key_columns[i] = in_block->get_by_position(result_column_id).column.get();
    }
    // ...一些初始化...
    RETURN_IF_ERROR(std::visit(
            [&](auto&& agg_method) -> Status {
                if (auto& hash_tbl = agg_method.data; hash_tbl.add_elem_size_overflow(rows)) {
                    // 如果需要扩展预聚合哈希表,则跳过预聚合,说明中间结果过多,可能是分组字段排列太分散,聚合效果不好
                    if (!_should_expand_preagg_hash_tables()) {
                        // ...一些初始化...
                        // 定长算子,这里实现比较奇怪,执行计划生成时已经写死了这里为true
                        if (_use_fixed_length_serialization_opt) {
                            for (int i = 0; i < _aggregate_evaluators.size(); ++i) {
                                auto data_type = _aggregate_evaluators[i]->function()->get_serialized_type();
                                // 处理value_columns
                                if (mem_reuse) {
                                    value_columns.emplace_back(std::move(*out_block->get_by_position(i + key_size).column).mutate());
                                } else {
                                    value_columns.emplace_back(_aggregate_evaluators[i]->function()->create_serialize_column());
                                }
                                data_types.emplace_back(data_type);
                            }

                            for (int i = 0; i != _aggregate_evaluators.size(); ++i) {
                                // 逐个聚合函数进行value_columns的处理,分批进行,流式预聚合结束后会将分批的中间结果汇总
                                RETURN_IF_ERROR(_aggregate_evaluators[i]->streaming_agg_serialize_to_column(in_block, value_columns[i], rows,_agg_arena_pool.get()));
                            }
                        } else {
                            // 由于写死了定长算子,忽略变长实现
                        }
                        // out_block的数据非空,就使用内存重用
                        if (!mem_reuse) {
                            // out_block数据为空,直接写入
                            ColumnsWithTypeAndName columns_with_schema;
                            for (int i = 0; i < key_size; ++i) {
                                columns_with_schema.emplace_back(key_columns[i]->clone_resized(rows), _probe_expr_ctxs[i]->root()->data_type(), _probe_expr_ctxs[i]->root()->expr_name());
                            }
                            for (int i = 0; i < value_columns.size(); ++i) {
                                columns_with_schema.emplace_back(std::move(value_columns[i]),data_types[i], "");
                            }
                            out_block->swap(Block(columns_with_schema));
                        } else {
                            // out_block数据非空,利用COW创建新的Block拷贝,并将key_column的数据按列批量拷贝
                            for (int i = 0; i < key_size; ++i) {
                                std::move(*out_block->get_by_position(i).column).mutate()->insert_range_from(*key_columns[i], 0, rows);
                            }
                        }
                    }
                }
                return Status::OK();
            },
            _agg_data->_aggregated_method_variant));

    // 如果需要扩展预聚合哈希表,在这里直接进行全量聚合,而不是流式预聚合
    if (!ret_flag) {
        RETURN_IF_CATCH_BAD_ALLOC(_emplace_into_hash_table(_places.data(), key_columns, rows));
        for (int i = 0; i < _aggregate_evaluators.size(); ++i) {
            RETURN_IF_ERROR(_aggregate_evaluators[i]->execute_batch_add(in_block, _offsets_of_aggregate_states[i], _places.data(), _agg_arena_pool.get(), _should_expand_hash_table));
        }
    }
    return Status::OK();
}

以AVG为例,streaming pre-agg聚合流程相关整体实现:

— AggregationNode::get_next |-executor::pre_agg(实际上是_pre_agg_with_serialized_key)
|–VExprContext::execute // 获取key_column
|–AggFnEvaluator::streaming_agg_serialize_to_column //
进行value_column计算
|—AggregateFunctionAvg::streaming_agg_serialize_to_column |
|–executor::get_result(实际上是_get_with_serialized_key_result)
|—AggregationMethodKeysFixed::insert_keys_into_columns //
将key_column和value_column整合为结果Block
|----AggregationMethodKeysFixed::insert_key_into_columns
|-----IColumnDummy::insert_data

Block与聚合算子

下面简要分析聚合算子中Block数据结构的作用,首先来介绍聚合函数中的各种抽象实现。Doris的聚合函数均继承了IAggregateFunctionDataHelper,每种聚合函数实现自己的数据域和处理函数,在接口类中声明了各种虚函数用于处理聚合过程中的计算和序列化/反序列化,这里只列出一些比较重要的声明:

class IAggregateFunction {
public:
    //...忽略一些函数声明...
    // 将一些column数据add到聚合函数的数据域中,可能会包含一些计算的操作,如sum或count
    virtual void add(AggregateDataPtr __restrict place, const IColumn** columns, size_t row_num,
                     Arena* arena) const = 0;
    // merge聚合状态
    virtual void merge_vec(const AggregateDataPtr* places, size_t offset, ConstAggregateDataPtr rhs,
                           Arena* arena, const size_t num_rows) const = 0;
    // 数据传输前,进行序列化操作
    virtual void serialize(ConstAggregateDataPtr __restrict place, BufferWritable& buf) const = 0;
    virtual void serialize_vec(const std::vector<AggregateDataPtr>& places, size_t offset,
                               BufferWritable& buf, const size_t num_rows) const = 0;
    // 反序列化数据
    virtual void deserialize(AggregateDataPtr __restrict place, BufferReadable& buf,
                             Arena* arena) const = 0;
    virtual void deserialize_vec(AggregateDataPtr places, const ColumnString* column, Arena* arena,
                                 size_t num_rows) const = 0;
    // 将给定的聚合结果放入Column
    virtual void insert_result_into(ConstAggregateDataPtr __restrict place, IColumn& to) const = 0;
    virtual void insert_result_into_vec(const std::vector<AggregateDataPtr>& places,
                                        const size_t offset, IColumn& to,
                                        const size_t num_rows) const = 0;
    // 批量add,其实就是for循环add
    virtual void add_batch(size_t batch_size, AggregateDataPtr* places, size_t place_offset,
                           const IColumn** columns, Arena* arena, bool agg_many = false) const = 0;
    // 流式预聚合序列化处理
    virtual void streaming_agg_serialize(const IColumn** columns, BufferWritable& buf,
                                         const size_t num_rows, Arena* arena) const = 0;
    //...忽略一些函数声明...
};

在函数声明中可看到Doris使用了自行实现的Arena类进行基于内存池的内存分配管理,Arena分配的是连续内存,提供了高效的访问效率,能够利用Cache且易于管理。如Distinct处理过程可能会高频访问待处理数据以进行去重,因此merge、add、反序列化等操作使用了Arena进行内存管理,以尽量将数据保持在Cache中,提高访问效率。Arena也使用了Doris自行实现的Allocator进行内存分配,根据Doris开发人员的说法,诸如此类内存分配、代码结构优化的调优可能比实现向量化本身有更好的性能优化效果。

数据域实现相对简单,如SUM聚合函数的数据域实现:

template <typename T>
struct AggregateFunctionSumData {
    T sum {};
    // 累加结果
    void add(T value) { sum += value; }
    // 合并其他SUM数据域的结果
    void merge(const AggregateFunctionSumData& rhs) { sum += rhs.sum; }
    // 序列化SUM数据
    void write(BufferWritable& buf) const { write_binary(sum, buf); }
    // 反序列化SUM数据
    void read(BufferReadable& buf) { read_binary(sum, buf); }
    ///...忽略一些声明...
};

聚合以分批的方式进行,每次聚合一个batch的数据,根据不同的聚合函数实现,进行不同的处理函数,如SUM是比较简单的累加,AVG则涉及到Sum和Count,并在Finalize中进行Sum与Count相除的结果计算。

Scan过程分析

SCAN的整体执行过程也使用Block数据结构作为数据流的基本单位,也实现了Doris自身的延迟物化、filter等特性,在filter中引入了SIMD的一些执行逻辑,大致代码调用关系如下,下面进行展开分析。

VScanNode::get_next ScannerContext::get_block_from_queue // 从block
queue中获取block
— ScannerScheduler::_scanner_scan // scanner线程 VScanner::get_block |-VFileScanner::_get_block_impl |–ParquetReader::get_next_block
|—RowGroupReader::next_batch |----RowGroupReader::_do_lazy_read
|-----RowGroupReader::_read_column_data // 读取谓词Column,计算过滤信息
|-----RowGroupReader::_read_column_data // 读取其他Column
|-----RowGroupReader::_fill_partition_columns // 物化Column
|-----RowGroupReader::_fill_missing_columns // 填充默认值或NULL
|-----RowGroupReader::_build_pos_delete_filter // 构建过滤器
|------ScalarColumnReader::read_column_data
|-------ScalarColumnReader::_read_values
|--------LevelDecoder::get_next_run |---------RleDecoder::GetNextRun |
|–RowGroupReader::_filter_block // 对Scan得到的Block开始过滤
|—RowGroupReader::_filter_block_internal |----ColumnArray::filter
|-----ColumnArray::filter_generic |------ColumnVector::filter
|-------simd::bytes32_mask_to_bits32_mask //进行simd过滤,过滤掉不需要的数据
ScannerContext::append_blocks_to_queue // 将scanner线程获取到的block数组放入队列

Scan过程主要使用了下面几个类:

VScanner:负责对tablet中的一部分数据进行读取;
ScannerContext:负责协调同一VScanNode下多个VScanner,其中记录了VScanner的执行状态,并维护了一个block队列用于存放VScanner读取的数据;
ScannerScheduler:负责对VScanner进行调度和执行,ScannerContext是其调度的基本单元。其中定义了用于存放ScannerContext的pending队列和两种类型的线程池,即用于对ScannerContext进行调度的调度线程池和用于执行读取任务的执行线程池;
VScanNode:负责从ScannerContext中的block队列中读取数据; RowGroupReader:对file
reader的读取请求进行具体实现,进行数据物化,填充Block;
ScalarColumnReader/ArrayColummReader:对于标量或其他复杂类型进行数据读取的具体实现;
GenericReader:各种类型file reader的父类;
ColumnArray:基于IColumn::Filter对读取完成的数据进行过滤,使用SIMD优化;
Block:基础数据结构,利用Copy-on-Write技术封装了IColumn;

Doris的Scan过程大致与Impala类似,但也有所不同,Doris由scanner线程进行Scan的具体执行,并由ScannerScheduler进行scanner线程的调度,使用ScannerContext来管理Scan过程的上下文。ScanNode的读取过程类似生产者-消费者模型,scanner线程完成读取的数据会以Block的形式压入读取队列,然后在调用VScanNode::get_next时会从队列中拿出Block,并进行后续的过滤处理。
在这里插入图片描述
先来重点看scanner线程执行的部分,Doris在Scan时实现了Block的内存复用,每次申请空Block是从特定的pool中获取一个之前已经申请好内存的Block,Scan结束后Block会清空并放回pool,这样可以提升Scan时内存分配的效率,能够让Block尽量呆在Cache中。从被扫描表的元数据中可以得到TupleDescriptor(这个类和Impala中的相同,只是改了名字),随后根据被扫描表的TupleDescriptor来逐个遍历slot,然后利用slot中的字段名、类型等信息来构建空的IColumn对象。这里虽然复用的是Impala原有的Tuple数据结构,但Doris也实现了一些用来帮助转换为Block以及IColumn的helper函数,可以比较方便地利用Tuple来构造ColumnWithTypeAndName,即Block中的各个IColumn存储单位。
在这里插入图片描述
构造完成空Block后,会调用VFileScanner::_get_block_impl来对Block进行填充,根据当前文件的schema来使用不同的reader进行数据读取,支持Parquet、Iceberg、Orc、CSV、Json等类型的文件读取,这里先分析使用较为广泛的Parquet reader。Parquet的读取过程是逐个Column进行的,在正式读取数据之前,Doris会先根据Column类型进行判断,如果不是复杂类型且存在谓词,则进行谓词Column的预读操作,根据预读出来的数据和Scan中的谓词去计算一个filter_map,然后进行其他Column的读取,如果之前在预读数据中能够通过谓词过滤掉超过60%的数据,就会进入skip过程,表明这个Scan的谓词可能会过滤掉整个page,相关代码:

bool skip_whole_batch = false;
if (select_vector.has_filter() && select_vector.filter_ratio() > 0.6) {
    // lazy read
    size_t remaining_num_values = 0;
    for (auto& range : read_ranges) {
        remaining_num_values += range.last_row - range.first_row;
    }
    if (batch_size >= remaining_num_values &&
        select_vector.can_filter_all(remaining_num_values)) {
        select_vector.skip(remaining_num_values);
        _current_row_index += _chunk_reader->remaining_num_values();
        RETURN_IF_ERROR(_chunk_reader->skip_page());
        *read_rows = remaining_num_values;
        if (!_chunk_reader->has_next_page()) {
            *eof = true;
        }
        return Status::OK();
    }
    skip_whole_batch = batch_size <= remaining_num_values && select_vector.can_filter_all(batch_size);
    if (skip_whole_batch) {
        select_vector.skip(batch_size);
    }
}

谓词下推的page过滤完成后,根据不同的Parquet encoding level(字典、RLE编码)来读取Parquet文件,返回的数据会逐个Column进行物化以及默认值或NULL值的设置。读取完成后,会逐个Column进行基于SIMD的过滤,在filter_map的生成过程中,会根据带有谓词的Column的过滤结果来生成column_filter,并使用基于SIMD的bytes32_mask_to_bits32_mask函数来根据column_filter进行谓词过滤,这里会把所有数据过滤完成,区别于上文中仅进行的page过滤,相关代码:

template <typename T>
ColumnPtr ColumnVector<T>::filter(const IColumn::Filter& filt, ssize_t result_size_hint) const {
    /*
    ...一些初始化和校验
    */

    // filt是谓词Column扫描过程中生成的过滤器,是一个用于标记过滤行号的bit数组
    const UInt8* filt_pos = filt.data();
    const UInt8* filt_end = filt_pos + size;
    const T* data_pos = data.data();
    static constexpr size_t SIMD_BYTES = 32;
    const UInt8* filt_end_sse = filt_pos + size / SIMD_BYTES * SIMD_BYTES;
    
    // 在SIMD实现分析文档有类似案例,每次进行32个byte的过滤,剩余部分用串行方式完成
    while (filt_pos < filt_end_sse) {
        // mask是把filt_pos通过位运算转换得到的uint32整数,其二进制为filt_pos的32个uint8元素的值
        uint32_t mask = simd::bytes32_mask_to_bits32_mask(filt_pos);
        // 全1则全保留
        if (0xFFFFFFFF == mask) {
            res_data.insert(data_pos, data_pos + SIMD_BYTES);
        } else {
            while (mask) {
                // 逐位判断,利用GCC的内置函数计算末尾0个数,从而确认1所在位置下标
                const size_t idx = __builtin_ctzll(mask);
                res_data.push_back_without_reserve(data_pos[idx]);
                mask = mask & (mask - 1);
            }
        }

        filt_pos += SIMD_BYTES;
        data_pos += SIMD_BYTES;
    } 
    // ...串行处理剩余数据...
    return res;
}

整体来看,Doris的向量化执行引擎在Scan逻辑上体现的主要是基于数据结构的实现,数据的读取、处理、优化等操作均围绕基于Column的Block数据结构来实现,其线程调度方式与向量化无太大关系,但在2.0-alpha版本中提出的pipeline执行引擎中Doris的主要算子使用了Push式的数据流,SQL执行过程有了比较大的变化,从官方给出的描述来看,Doris能够比较好地解决复杂查询的fragment instance堆积问题。

总结

Doris的向量化引擎大致来看属于两部分的实现,一部分是以SIMD为主的细粒度优化,主要面向偏底层数据处理过程的加速,使用特定指令集的SIMD指令,可以提高定长数据结构批量处理的效率。另一部分是数据结构和执行引擎整体的重构改造,由于数据流的基础单位从RowBatch变为Block,所有算子均需要将内部处理逻辑从RowBatch-Tuple变为以Block-Column为基础的向量化架构。除此之外,Doris还实现了内存分配、弹性扩缩容数组、COW指针等特性,用于优化基础数据结构读写操作的执行效率以及其内存分配与释放。

参考文章

https://zhuanlan.zhihu.com/p/325632066

https://zhuanlan.zhihu.com/p/454141438

https://www.cnblogs.com/happenlee/p/14990049.html

https://www.6aiq.com/article/1666874991171

https://cloud.tencent.com/developer/article/2111901

https://blog.bcmeng.com/post/doris-bitmap.html

https://mp.ofweek.com/it/a056714509327

https://www.slidestalk.com/doris.apache/SQL82045?video

https://doris.apache.org/zh-CN/docs/dev/summary/basic-summary

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Doris和ClickHouse是两个流行的开源分布式列式存储数据库,它们都支持向量化(Vectorization)技术。向量化是一种优化技术,通过处理数据的向量(数组)而不是单个元素,以提高查询和计算的效率。 在传统的处理方式中,数据库系统通常会逐个处理数据,即逐行或逐列进行操作。而向量化技术则将一组数据(向量)作为单个单元进行处理,以实现更高的并行度和更好的硬件资源利用率。以下是向量化的一些关键概念和特点: 1. 批处理:向量化技术通常以批处理的方式工作,即一次处理多个数据项。这样可以减少函数调用和循环的开销,并利用SIMD(单指令多数据)指令集进行并行计算。 2. 矢量化操作:向量化技术可以将一组数据应用于相同的操作,例如加法、乘法或逻辑运算等。通过将操作应用于整个向量,可以减少指令的开销,并提高计算效率。 3. 数据压缩:向量化技术通常与数据压缩相结合,以减少内存和存储开销。通过对向量进行压缩,可以减少数据传输和存储的需求,并提高整体性能。 向量化技术在Doris和ClickHouse中的应用主要体现在查询和计算操作上。通过使用向量化技术,这些数据库可以更高效地执行复杂的分析查询、聚合操作和向量运算。这对于处理大规模数据集和高并发负载非常有益,可以显著提高查询性能和系统吞吐量。 需要注意的是,向量化技术的效果取决于具体的使用场景和数据特征。因此,在选择数据库时,建议根据自己的需求和实际情况评估向量化技术对性能的影响。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值