過去 10 年,數據分析基準的成績已經提升了數十倍。這種性能的提升造就了商業世界中更大的可能——從特定維度的 MOLAP 分析和周期報表,到隨時隨地從任意維度分析中發掘新范式的 Ad-hoc 查詢,直到現在基于 Agent 派生出的復雜查詢、高并發 + 高性能需求。基于日益實時、智能的 OLAP 引擎,企業的數據資產正在產生更大的價值。
從簡單、固定、分鐘到小時級別的查詢,到亞秒級、PB 級數據、大寬表、高并發、復雜 JOIN 和聚合,我們為何在今天能夠實現當初不敢想象的分析需求?
Apache Doris 的演進給我們提供了一個生動的答案——它不僅跟隨硬件與編譯器的發展而演進,更主動地通過向量化、模板化、指令級并行與精細的用戶態調度模式,將每一代 CPU 的潛力推向理論極限。
1. Background:硬件與編譯器的變遷
這十年硬件與編譯器的發展軌跡,徹底改變了高性能數據庫的設計哲學:
- CPU:從“更快”到“更寬、更多”
- 主頻停滯,核心爆發:單個核心的主頻已在 3-5GHz 徘徊多年,性能提升轉而依賴核心數量(從個位數到百核級)和單核心的并行寬度(SIMD)。
- 內存墻加劇:CPU 與主存的速度差距已超 300 倍。緩存命中率成為性能的生命線,一次緩存未命中(Cache Miss)的代價足以執行數百條指令。
- SIMD 成為標配:AVX2(256 位)、AVX-512(512 位)及 ARM SVE 等指令集,允許單條指令處理 4 至 16 個數據單元。不用 SIMD,就等于主動浪費超過 80%的浮點算力。
- 編譯器:從“翻譯者”到“優化合作伙伴”
- 激進的內聯與向量化:現代編譯器(如 Clang/GCC)能在編譯期進行循環展開、分支消除、自動向量化,但其優化能力極度依賴代碼模式。虛函數、指針別名、分支預測失敗會瞬間阻斷其優化通路。
- 跨平臺抽象:通過優良的代碼模式,編譯器能夠自動為循環生成 SIMD 代碼,無縫遷移不同平臺。但對于復雜的數據處理邏輯,仍然需要手動生成 SIMD 代碼。
- 新硬件下的 OLAP 性能陷阱
-
基礎設施的升級迭代,意味著傳統數據庫引擎的微觀開銷被急劇放大:
-
火山模型:每行的虛函數調用,導致指令緩存(I-Cache) 被頻繁沖刷,核心處于“饑渴”狀態。
-
動態分支:在數據密集的循環中,一次預測失敗可能清空長達 15-20 級的指令流水線。
-
隨機內存訪問:不連續的內存訪問模式,讓 CPU 的預取器(Prefetcher)失效,大部分時間在等待數據從內存加載。
-
因此,性能優化必須從“算法優化”下沉為“與硬件和編譯器的對話”。Apache Doris(及其商業化版本)不斷在各類主流 benchmark 上取得令人驚嘆的領先(例如,ClickBench#1,JsonBench#1,RtaBench#1),背后正是這些與現代硬件體系協同進步的決心。
2. 向量化執行:從“行”到“列”的降維打擊
現代的 CPU 架構中,存在著大量激進如“黑科技”般的優化手段,例如指令亂序發射(OOE)、多級流水線、向量指令集(AVX、Neon、SVE)。它們能夠使你的代碼在同等的算力下性能倍增——前提是,你沒有反模式。
- 指令亂序發射與流水線:現代 CPU 的流水線深度可達 15-20 級,并通過亂序執行引擎動態調度指令。互相沒有依賴的指令雖然在匯編與機器碼中有先后順序,但實際可以完全并行。其性能發揮的關鍵在于指令流的連續性和可預測性。一次錯誤的分支預測會導致整個流水線被重置,帶來約 15 個時鐘周期的懲罰;而一次緩存未命中(Cache Miss) 導致的數百周期等待,更會讓所有精巧設計瞬間歸零。[1]
- 微指令緩存(μop Cache):μop Cache 是處理器前端的一種硬件緩存結構,能夠緩存熱指令,跳過解碼階段,提升每周期指令數(IPC)。根據常規統計,μop Cache 能夠產生穩定、可觀(2% ~ 10%)的 IPC 增加。但在目前常見的數據應用中,實際命中率從 30% 到 70% 差距很大[2],是明顯的性能提升點。
- 向量指令集(SIMD):這是性能提升一個數量級的核心武器。從 SSE 的 128 位到 AVX2 的 256 位,再到 AVX-512 的 512 位,意味著單條指令可同時處理的數據量從 4 個 32 位整數躍升至 16 個。理論峰值算力因此呈倍數增長。例如,在理想的數據密集循環中,使用 AVX-512 相比標量代碼可帶來 10 倍以上的吞吐量提升。
早期的數據庫執行引擎多基于火山模型(Volcano Model),以 Tuple(行)為單位進行處理。在 CPU 主頻停滯、核心數增加的今天,這種方式導致了大量的虛函數調用和糟糕的指令緩存命中率。這是因為在逐行處理的情況下,我們需要頻繁地切換處理對象(列)的類型,執行不同的操作。這導致指令緩存命中率極低,且無法利用向量化指令進行批量處理,逐行的虛函數開銷最高可達常規執行的數十倍[3][5]。
Doris 的全面向量化重構,核心在于引入了Block和Column的概念。這是內存布局的重大變革:
2.1 內存布局與 Cache Locality
在 Doris 的向量化引擎中,一列數據在內存中是連續存儲的。例如一個INT類型的列,直接使用 Doris 中的PODArray(Plain Old Data Array)來存儲數據。
// 核心邏輯示意
template <typename T>
class ColumnVector final : public IColumn {
private:
// PaddedPODArray 保證了內存對齊,通常按 64 字節對齊以適配 Cache Line
PaddedPODArray<T> data;
public:
// 向量化計算入口,不再處理單行,而是處理整個 data 數組
void filter(const Filter& filt) override {
// ...
}
}
這種布局保證了同一列的數據在物理上連續。當 CPU 從內存加載數據到 Cache 時,能夠一次性加載多個數據項,極大提升了指令/數據緩存的局部性(Cache Locality)。由于近些年 CPU 數據 Cache 容量大幅提升,常規運算的完成較大程度依賴在 L2 cache 中完全裝載數據。如果因為數據不連續頻繁刷新緩存,可能帶來 3~5 倍的局部性能損失[3][6]。
在傳統批處理模型下,Next()接口一次返回的Block 通常包含 4096 行,那么就會發生 4096 次的虛函數調用。而在向量化代碼中,整個 Column 每次一起處理,虛函數調用僅發生一次。在復雜算子(如 Hash Join、Aggregation)中,這種調用開銷的降低是數量級的區別。

2.2 自動與手動向量化的結合
隨著編譯器進化至今,編譯優化的技術同數據庫一樣有了飛躍式的進步。在更多的場景下,編譯器已能實現以前無法自動進行的優化。自動向量化(Auto Vectorization)就是其中的重要方面。
在絕大多數情況下,利用編譯器自動生成向量化的代碼是最佳的方案——它天然帶來了跨平臺遷移的友好,代碼也一目了然。而這并不意味著相關代碼的工程難度降低,許多時候反而更加復雜——因為最終執行的代碼向量化程度不再是編碼時的“所見即所得”,其中涉及到了復雜的編譯器行為。要保證最高的代碼生成質量,開發者編碼時必須盡可能遵守良好的開發范式。
當然,在一些更為復雜的場景下,手動調用的向量化代碼仍是不可避免的。因此 Doris 采用了“自動+手動”的雙重策略:
- 自動向量化:對于絕大多數情況下,這是現代編譯器提供給我們的最佳選擇。核心在于,簡化循環體,抽離控制流分支(Branch),讓編譯器識別出 Auto Vectorization 的機會。
- 手動向量化:對于實際匯編識別出未能正常 Auto Vectorization 的算子,或是那些熱點路徑上的向量化需求,Doris 工程師并不避諱直接手寫 Intrinsic 代碼。相比于自動生成的代碼,此種方式往往擁有更加極致的性能,幾乎沒有指令浪費。
例如以下場景:
A. 謂詞過濾(Filter)
在WHERE子句處理中,過濾結果通常是一個uint8_t的數組(0 或 1)。將過濾后的數據拷貝到新 Block 是高頻操作。 Doris 利用 AVX2 的 _mm256_movemask_epi8 指令,快速生成選擇掩碼,并配合 _mm256_permutevar8x32_epi32 等指令進行數據重排(Shuffle),避免了傳統分支判斷帶來的流水線沖刷。在相同的指令周期內,實現更大的數據吞吐量。
B. 字符串與 JSON 處理
字符串匹配(Like)、JSON 解析是 CPU 密集型操作。Doris 引入了特定的 SIMD 算法或者庫:
- Volnitsky 算法:在子串查找中,利用 SIMD 并行比較多個字符,快速跳過不匹配區域。
- SimdJson:在解析 JSON Path 時,利用 SIMD 指令快速定位結構符(如
{,},:),大幅縮短解析路徑。
// SIMD 子串匹配
const uint8_t* _search(const uint8_t* haystack, const uint8_t* haystack_end) const {
......
while (haystack < haystack_end && haystack_end - haystack >= needle_size) {
#if defined(__SSE4_1__) || defined(__aarch64__)
if ((haystack + 1 + n) <= haystack_end && page_safe(haystack)) {
/// find first and second characters
const auto v_haystack_block_first =
_mm_loadu_si128(reinterpret_cast<const __m128i*>(haystack));
const auto v_haystack_block_second =
_mm_loadu_si128(reinterpret_cast<const __m128i*>(haystack + 1));
const auto v_against_pattern_first =
_mm_cmpeq_epi8(v_haystack_block_first, first_pattern);
const auto v_against_pattern_second =
_mm_cmpeq_epi8(v_haystack_block_second, second_pattern);
const auto mask = _mm_movemask_epi8(
_mm_and_si128(v_against_pattern_first, v_against_pattern_second));
/// first and second characters not present in 16 octets starting at `haystack`
if (mask == 0) {
haystack += n;
continue;
}
......
}
3. 模板編譯:消除運行時開銷
前面我們討論了分支預測、數據和指令緩存、指令流水線這些“看得見摸不著”的性能關鍵點。那么,一次虛函數調用究竟會產生多大的開銷?可以用一個小例子來說明:
class VirtualBase {
virtual int foo(int x);
};
class VirtualDerived : public VirtualBase {
int foo(int x) override;
};
class NonVirtual {
int bar(int x);
};
static void BM_VirtualCall(benchmark::State& state) {
VirtualBase* obj = new VirtualDerived();
for (auto _ : state) {
result = obj->foo(42);
}
}
static void BM_NonVirtualCall(benchmark::State& state) {
NonVirtualBase obj;
for (auto _ : state) {
result = obj.bar(42);
}
}
static void BM_DirectCall(benchmark::State& state) {
VirtualDerived obj;
for (auto _ : state) {
result = obj.foo(42);
}
}

以上結果產生自 Clang++ 17.0 -O3 -std=c++20 條件下的測試,可以看到,虛函數調用導致了普通函數 5 倍的性能開銷。
因此,如何盡可能消除虛函數是 OLAP 領域中的重要課題。過去的研究展現了兩種常見的大方向:編譯執行和向量化執行。Doris 選擇的是向量化執行的方案,它通過一次處理一個 Block 的數據,將這些虛函數和分支的 overhead 均攤到數千行上。那么,有沒有一種方式,能夠在向量化執行的基礎上,像編譯執行一樣徹底消除掉所有的分支 overhead 呢?答案是:有的。
3.1 模板的藝術
編譯執行的本質是對于不同的類型、分支等代碼路徑,生成在當前條件下完全確定的代碼,從而消去不同類型所需的判斷和虛表訪問。
例如對于一個a + b的操作,編譯執行并沒有一個通用的add(Value a, Value b)函數,而是為int + int、double + double生成了完全獨立的機器碼。CPU 在執行時,不僅沒有虛函數指針跳轉,甚至可以將簡單的加法指令直接內聯(Inline),從而充分利用指令流水線。
在“編譯執行”框架下,這往往通過 LLVM JIT 等代碼生成框架完成,針對用戶的表達式現場生成一套完全固定的匯編并執行。但在“向量化執行”框架下,這同樣可以通過 C++ 的模板編程實現。例如對于 days_add 函數:
template <PrimitiveType PType>
struct AddDaysImpl {
......
static inline ReturnNativeType execute(const InputNativeType& t, IntervalNativeType delta) {
// PType 已經固定,不需要運行期判斷
return date_time_add<TimeUnit::DAY, PType, IntervalNativeType>(t, delta);
// compare to
// if (t.is_date) {
// return date_time_add<TimeUnit::DAY, DATEV2, IntervalNativeType>(t, delta);
// } else {
// return date_time_add<TimeUnit::DAY, DATETIMEV2, IntervalNativeType>(t, delta);
// }
}
// 不同模板實例參數不同,函數匹配時直接命中對應實例
static DataTypes get_variadic_argument_types() {
return {std ::make_shared<typename PrimitiveTypeTraits<PType>::DataType>(),
std ::make_shared<typename PrimitiveTypeTraits<IntervalPType>::DataType>()};
}
}
using FunctionAddDays = FunctionDateOrDateTimeComputation<AddDaysImpl<TYPE_DATEV2>>;
using FunctionDatetimeAddDays = FunctionDateOrDateTimeComputation<AddDaysImpl<TYPE_DATETIMEV2>>;
factory.register_function<FunctionDatetimeAddDays>();
factory.register_function<FunctionAddDays>();
可以看到,對于不同的入參類型(DATE 和 DATETIME),函數在參數匹配時已經命中了不同的實例,這些實例各自包含確定的類型信息,規避了運行期的虛表訪問,使原本無法內聯的函數變為可能。

4. 多線程內存分配:Jemalloc 與 Arena 的協同
這些年 CPU 發展的新趨勢是——主頻、單核心性能增長相對緩慢,CPU 核心數卻在不斷增長[4]。尤其是在逐漸占領市場的 ARM 架構下,核心數量更是呈指數級倍增,從 2018 年之前的 16 核以內,一路達到了現在 192 核的高峰[7]。這意味著我們必須把更多的目光投向高并發(High Concurrency)場景。
這種場景中,系統的瓶頸往往不在計算,而在 malloc/free 鎖競爭(Lock Contention)以及 TLB(Translation Lookaside Buffer)的刷新開銷。典型 OLAP 查詢會創建大量短生命周期對象(Hash Key、聚合狀態、臨時字符串)。如果這些都通過 glibc malloc/free 申請:
- 每次都走系統分配器,鎖競爭嚴重;
- 碎片多,RSS(常駐內存集,Resident Set Size)難以控制。
由于鎖護送(Lock Convoy)等效應的存在,隨著 CPU 核心數增加,多線程競爭甚至可能導致多核吞吐不升反降[8]。一旦 L1、L2 緩存被驅逐,就又是 3-5 倍的數據訪問開銷。在內存分配上,這種性能瓶頸尤為突出。為避免全局競爭、有鎖分配是 Doris 必須解決的技術問題。
4.1 接管全局分配器
因此,Doris 后端進程(BE)選擇了鏈接 Jemalloc 進行內存分配。其核心優勢在于 Thread Local Cache (Tcache)。每個執行線程擁有獨立的內存分配緩存,絕大多數小對象的申請無需加鎖,消除了全局鎖競爭。許多小對象申請直接通過 Jemalloc 緩存解決,不進行系統調用。
4.2 Arena 內存池
但在查詢執行內部,Doris 并沒有止步于此。在執行算子(如 Hash Join、Aggregation)時,Doris 使用 Arena(區域內存池)模式,這是因為很多對象的生命周期和“查詢”綁定,完全可以在查詢結束時統一回收。這直接帶來了若干收益:
- 無鎖分配:算子內部申請內存通常只是當前線程緩存內的指針簡單移動(Bump Pointer),完全無鎖。
- 批量釋放:查詢結束后,整塊 Arena 統一釋放,避免了數百萬次小對象的析構開銷。
- Cache 友好:同一算子使用的對象在內存中緊湊排列,極大提升了 CPU 緩存命中率。
class Arena : private boost::noncopyable {
struct Chunk : private Allocator<false> {
......
}
public:
char* alloc(size_t size) {
_init_head_if_needed();
if (UNLIKELY(head->pos + size > head->end)) {
_add_chunk(size);
}
// 直接 bump pointer,開銷無限小
char* res = head->pos;
head->pos += size;
return res;
}
// 一次性統一回收
void clear(bool delete_head = false) {
......
}
};

5. Pipeline 執行引擎:解決多核時代的調度瓶頸
傳統的火山模型對于每個 Instance 使用獨立的線程進行處理,每個線程需要處理一個完整 Fragment(查詢計劃片段)的部分數據。顯然,這時的任務調度完全依賴操作系統的線程調度,而這在 OLAP 場景下存在很多根本性問題:
- 如果一個線程因為網絡或磁盤 IO 阻塞,操作系統就會進行線程的上下文切換(Context Switch),開銷在微秒級別。隨著查詢數量和規模增長,系統線程數暴漲,導致上下文切換頻繁,overhead 明顯增加;
- 無法細粒度實現 query 之間的公平調度。大小查詢混合場景下,小查詢被調度到的機會明顯下降,延遲大幅增高;
- 線程頻繁遷移,喪失 NUMA 和 Cache 親和性,影響查詢性能;
- 依賴底層數據分布,無法交換數據,數據傾斜對性能影響巨大
……
核心問題是,依附于線程模型的 Instance 執行,其調度完全依賴操作系統,無法進行更細粒度的調整。由阻塞、親和性、優先級帶來的影響隨著現代 CPU 核數不斷增加、系統負載不斷增高,嚴重性也逐步提升。在現代 CPU 上,單次 Context Switch 往往帶來上千個指令周期的時間成本,近似于上千次浮點運算[3]。而這還不是最糟糕的——如果發生了跨核心遷移,更是會花費數微秒的代價用于緩存重建和 CPU Core 之間同步。高競爭環境下,CPU 可能有數個百分點的時間都花在這些非運算代價中。[9]
Doris 通過全新設計的 Pipeline 引擎,在用戶態實現精細化的調度控制,終于解決了以上全部問題。本質上講,它實現了一套完整的協程(Coroutine)語義,也就是用戶態調度。極其符合 OLAP 負載的實際。
5.1 阻塞等待?Pipeline Task 拆分
查詢計劃根據阻塞算子拆解為多個 Pipeline,每個 Pipeline 包含一組算子(Operator)。所有阻塞算子的多個上游均被拆分至不同的 Pipeline,所以 Pipeline 內部完全不發生阻塞。每個邏輯 Pipeline 被實例化為多個物理 PipelineTask,可被多核同時調度以充分利用 CPU 資源。

這保證了所有阻塞的操作不會占用執行線程,而是標記自身阻塞狀態后,將當前線程交回給 Pipeline 調度器重新調度。因此,我們不再需要隨著查詢數量增多的線程了。
5.2 上下文切換?線程遷移?用戶態調度器
Doris 實現了一個類似于 Go Runtime(協程)的用戶態調度器(Task Scheduler),它包含:
- 就緒隊列(Runnable Queue):一旦數據就緒,依賴被滿足,Task 轉移至就緒隊列。可以隨時被調度執行。
- 阻塞隊列(Blocked Queue):當 Task 需要等待 IO 或 RPC 數據時,它被放入阻塞隊列,不占用操作系統線程。
- 執行線程池:一組固定數量的線程不斷從就緒隊列取出 Task 執行。執行線程綁核以保證 Cache 命中率。
在此基礎上,我們更進一步地迭代了新的 PipelineX 執行引擎,也就是 Doris 當前所使用的執行引擎。通過設置上下游 PipelineTask 之間依賴的方式進一步規避了對阻塞任務的輪詢,實現了自動喚醒下游可執行任務。

5.3 數據傾斜?細粒度數據均衡
我們前面說到,近年來 CPU 發展的特點是什么?更多的核心、更多的系統線程數。這意味著我們的同一個查詢,可以同時拆分成更多份進行并行。這是個好事兒……吧?
一般來說是的。更多的線程意味著單位時間更大的吞吐。但這明顯受到“短板效應”的制約——如果掃描的每個存儲分桶(Bucket/Tablet)數據量不一致,上層每個 PipelineTask 的執行時間也必然不一致。在不同的查詢負載下,僅通過調整分桶策略幾乎無法找到最優解。
在 Doris 的新 Pipeline 引擎中,解決這一問題卻很簡單:通過添加 Local Shuffle 算子,對 PipelineTask 之間的數據進行重新分布,“數據傾斜”問題被全自動化地消解了。

5.4 小查詢餓死?分時復用與搶占
為了防止大查詢餓死小查詢,Pipeline 引擎引入了基于多級反饋隊列的時間片輪轉機制。一個 Task 每次在 CPU 上執行的時間有限(例如 100ms),如果當前時間片運行完,必須出讓 CPU 給其他任務。同時,根據執行時間的累計,大的 Task 會被逐漸降級調度,保證小查詢比大查詢有更高的優先級,防止延遲被影響。
這種機制使得 Doris 在高并發混合負載下,CPU 利用率能夠穩定維持在 95% 以上,且完全避免了線程爆炸(Thread Explosion)導致的系統抖動。

6. 落地:可驗證的性能提升結果
所以,我們羅列的這些技術,到底有用沒有?是高大上的花活,還是真正能夠落地到成熟系統中的關鍵優化呢?
來吧,讓我們看一下 Apache Doris(及 VeloDB 等商業發行版)在各個優化前后的直接性能對比結果。經過長久的迭代,它們現在均已成為 Doris 堅實的架構基礎。
6.1 向量化執行
首先是向量化部分。在 1.2 版本,Doris 的向量化徹底成熟。相比于過去的火山模型,這是一次里程碑式的性能躍升——根據實驗,開啟向量化之后的 Doris 1.2 相比早期的 Doris 0.15,在 SSB-Flat 上性能提升了近 10 倍[10],在 TPC-H 上提升了超過 11 倍,最顯著的單個 SQL 提速更是達到了近 70 倍[11]。
6.2 Pipeline 執行引擎
在 Doris 2.0 版本上,我們實現了 Pipeline 執行引擎,并在 2.1 版本進行了大的重構,使全部實現達到理想狀態。在 Apache Doris 2.0 上的測試結果表明,Pipeline 引擎配合合理的 SQL 優化,達到了相比火山模型 100% 的 TPC-H 性能提升,相比于 Trino/Presto 更是有 3-5 倍的性能領先[12]。基于 Pipeline 引擎,Doris 更是引入了 Workload Group 進行資源劃分和負載控制,有效解決了大型公司中面對大量用戶復雜場景的穩定性問題。
在 Doris 2.1 中,我們的 Pipeline 引擎達到了最終形態,它具備了自適應解決數據傾斜的能力。相比于已經達到業內領先水平的 Doris 2.0,它在 TPC-DS 上進一步實現了 100% 的性能提升。在數據不均衡、分桶數極不合理的極端情況下重新測試 ClickBench 和 TPC-H,也幾乎不產生性能損失[13]。
6.3 ARM 架構優化
得益于 Doris 精細的向量化實現,ARM 架構下 Doris 的性能相比于其他產品,產生了比 X86 平臺更大的領先優勢。Doris 2.1 是第一個針對 ARM 架構深度優化的版本。相比于前一個版本,ARM 下的 Doris 2.1 在 ClickBench 上的成績提升了 230%,TPC-H 上也達到了接近 1 倍的性能提升。[14]
尤其是在 AWS Graviton4 架構下,Doris 憑借卓越的優化,相比于 X86 在 ClickBench、SSB、SSB-Flat、TPC-H、TPC-DS 上分別取得了 65%、54%、53%、54%、60% 的性價比提升[14]。昭示著 ARM 儼然成為了數據分析領域高性價比的選擇。

6.4 整體性能領先
相比于 Clickhouse、Trino 等其他 OLAP 分析引擎,Doris 的橫向對比成績究竟如何?經歷了這么多優化之后,是否真正取得了領先?以下是一些事實結果:
- 相比于 Clickhouse,Doris 在其自家維護的 ClickBench 上曾多次取得領先,上一次提交的成績位列第 2 名,領先于 Clickhouse 的第三名 2% 的總分。在 SSB、TPC-H 上,更是分別有 3 倍和 60 倍的性能領先。在 TPC-DS 上,Clickhouse 在同等資源下只能執行約 50% 的查詢,這部分成績比 Doris 的總成績還落后 1 倍[15]。
- 而在實時更新場景中,二者差距更大。根據發行商 VeloDB 的測試結果,在比 Clickhouse 更差的硬件條件下,25% 更新率場景下 Doris 比 Clickhouse 快 14 倍;100% 更新率時領先更是達到了 18 倍[16]。
- 相比于 Trino/Presto,Doris 在 TPC-DS 1TB 測試中使用同等條件進行數據湖查詢,達到了 3 倍的性能領先;使用 Doris 內表性能領先更是達到 10 倍之多。在實際用戶場景中,查詢延時更是降低了最多 20 倍[17]。
- 與 Spark 對比,Doris 在其擅長的復雜查詢下性能領先 4-6 倍,實時場景下更是實現了代際級別的延遲優勢[13]。
- 對比擅長半結構化數據存儲的 ElasticSearch,Doris 在半結構化測試集 JsonBench 上達到了 2 倍性能領先,同時超越了 Clickhouse。相比于 Postgresql 領先幅度更是達到 80 倍之多[18]。
總結
過去十年,OLAP 性能需求的演進,本質上是向底層要算力的一場硬仗。當查詢變得復雜、數據量暴漲、并發攀升時,傳統執行引擎在硬件層面的低效被無限放大。Apache Doris 團隊面對的,正是如何駕馭現代多核 CPU 與智能編譯器,將每一份硬件潛能轉化為穩定的性能提升。
挑戰是明確的:如何消除虛函數和分支預測帶來的開銷?如何讓內存訪問模式更適配 CPU 緩存?如何在高并發下避免鎖與調度成為瓶頸?Doris 的應對策略清晰而系統:
- 針對計算效率,我們通過全面的向量化重構和模板化編程,將處理單元從“行”升級為“列”,并在編譯期固化類型與分支,讓生成的代碼近乎直接匹配 CPU 的高效流水線。
- 針對內存效率,我們引入專用的內存分配器與池化技術,大幅削減高并發下的鎖競爭與碎片,確保數據在緩存中緊湊排列。
- 針對多核調度,我們自研 Pipeline 執行引擎,在用戶態實現精細的任務調度與數據均衡,徹底解決操作系統線程模型在 OLAP 場景下的固有缺陷。
這些優化不是孤立的技術堆砌,而是一套貫穿數據從加載到計算全鏈路的系統性工程。其核心在于,團隊始終保持著對硬件行為與編譯器邏輯的深刻理解,并以此驅動架構演進——讓代碼的寫法順應硬件的“脾氣”,讓執行路徑契合編譯器的“優化邏輯”。
最終,這使 Doris 能夠持續地將每一代 CPU 的理論算力,穩定地轉化為用戶場景下的實際吞吐與低延遲。性能的極致,來自于對底層細節的持續深耕與系統化掌控。
參考文獻
- https://www.abhik.xyz/concepts/performance/cpu-pipelines
- https://webs.um.es/aros/papers/pdfs/ssingh-isca24.pdf
- https://blog.codingconfessions.com/p/context-switching-and-performance
- https://www.servethehome.com/updated-amd-epyc-and-intel-xeon-core-counts-over-time
- https://faculty.cs.niu.edu/~winans/notes/patmc.pdf
- https://pikuma.com/blog/understanding-computer-cache
- https://www.phoronix.com/review/ampereone-aws-graviton4
- https://grokipedia.com/page/Non-blocking_algorithm
- https://www.systemoverflow.com/learn/os-systems-fundamentals/cpu-scheduling/what-is-cpu-scheduling-and-context-switching
- https://doris.apache.org/blog/ssb
- https://doris.apache.org/blog/tpch
- https://www.velodb.io/blog/milestone-apache-doris-2-0
- https://www.velodb.io/blog/apache-doris-2-1-0-released
- https://www.velodb.io/blog/apache-doris-achieves-70-better-price-performance
- https://doris.apache.org/docs/3.x/gettingStarted/alternatives/alternative-to-clickhouse
- https://www.velodb.io/blog/apache-doris-34x-faster-clickhouse-realtime-updates
- https://doris.apache.org/docs/3.x/gettingStarted/alternatives/alternative-to-trino
- https://medium.com/@VeloDB_poweredby_ApacheDoris/1-billion-json-records-1-second-query-response-apache-doris-vs-7bbe9d9e3a12

