ClickHouse中的CPU调度

图片

本文字数:14267;估计阅读时间:36 分钟

作者:Maksim Kita

审校:庄晓东(魏庄)

本文在公众号【ClickHouseInc】首发

图片

概述

在这篇文章中,我将描述向量化的工作原理,什么是CPU调度,如何找到CPU调度优化的空间,以及如何在ClickHouse中使用CPU调度。

首先,描述一下的问题。硬件供应商不断的向现代CPU的指令集中添加新指令。我们经常想使用最新的指令进行优化,其中最重要的是SIMD指令。但这样做主要的问题是兼容性。例如,如果你的程序是用AVX2指令集编译的,而你的CPU只支持SSE4.2,那么如果运行这样的程序,你将收到一个非法指令信号(SIGILL)。

还需要注意的一点是:可以专门设计适应应SIMD指令的数据结构和算法,例如现代整数压缩编解码器,或者稍后移植到这些指令,例如JSON解析。

为了在保持与旧硬件兼容的同时提高性能,代码的部分可以为不同的指令集编译,然后在运行时程序可以将执行分派到性能最佳的变体。

在本文的任何示例中,我将使用clang-15编译器。

向量化基础知识

向量化是一种优化,其中使用矢量操作,而不是标量操作处理数据。现代CPU具有特定的指令,允许您使用SIMD指令以矢量方式处理数据。这样的优化可以手动执行,也可以由编译器执行自动向量化。

让我们考虑以下代码示例:

void plus(int64_t * __restrict a, int64_t * __restrict b, int64_t * __restrict c, size_t size)
{for (size_t i = 0; i < size; ++i) {c[i] = b[i] + a[i];}
}

我们有一个plus函数,它接受指向abc数组的3个指针,以及这些数组的大小。该函数计算a和b数组元素的和,并将其写入数组c。

如果我们在没有循环展开的情况下编译此代码,通过指定选项fno-unroll-loops,并且启用AVX2通过选项-mavx2,将生成以下汇编:

$ /usr/bin/clang++-15 -mavx2 -fno-unroll-loops -O3 -S vectorization_example.cpp
# %bb.0:testq  %rcx, %rcxje  .LBB0_7
# %bb.1:cmpq  $4, %rcxjae  .LBB0_3
# %bb.2:xorl  %r8d, %r8djmp  .LBB0_6
.LBB0_3:movq  %rcx, %r8andq  $-4, %r8xorl  %eax, %eax.p2align  4, 0x90
.LBB0_4:                                # =>This Inner Loop Header: Depth=1vmovdqu  (%rdi,%rax,8), %ymm0vpaddq  (%rsi,%rax,8), %ymm0, %ymm0vmovdqu  %ymm0, (%rdx,%rax,8)addq  $4, %raxcmpq  %rax, %r8jne  .LBB0_4
# %bb.5:cmpq  %rcx, %r8je  .LBB0_7.p2align  4, 0x90
.LBB0_6:                                # =>This Inner Loop Header: Depth=1movq  (%rdi,%r8,8), %raxaddq  (%rsi,%r8,8), %raxmovq  %rax, (%rdx,%r8,8)incq  %r8cmpq  %r8, %rcxjne  .LBB0_6
.LBB0_7:vzeroupperretq

在最终的汇编中,有两个循环。向量化循环,每次处理4个元素:

.LBB0_4:                                # =>This Inner Loop Header: Depth=1vmovdqu  (%rdi,%rax,8), %ymm0vpaddq  (%rsi,%rax,8), %ymm0, %ymm0vmovdqu  %ymm0, (%rdx,%rax,8)addq  $4, %raxcmpq  %rax, %r8jne  .LBB0_4

标量循环:

.LBB0_6:                                # =>This Inner Loop Header: Depth=1movq  (%rdi,%r8,8), %raxaddq  (%rsi,%r8,8), %raxmovq  %rax, (%rdx,%r8,8)incq  %r8cmpq  %r8, %rcxjne  .LBB0_6

在函数汇编的开头,有一个检查,用于决定数组大小,从而选择用哪个循环:

# %bb.1:cmpq  $4, %rcxjae  .LBB0_3
# %bb.2:xorl  %r8d, %r8djmp  .LBB0_6

此外,需要注意的一点是vzeroupper指令。编译器插入这个指令是为了避免混合使用SSE和VEX AVX指令的惩罚。您可以在Agner Fog的《在汇编语言中优化子例程:x86平台优化指南》的第13.2节“混合VEX和SSE代码”中了解更多信息(https://www.agner.org/optimize/)。

另一个需要注意的重要事项是:输入数组指针上的__restrict关键字。它告诉编译器函数参数不会别名。这意味着它们特别不指向重叠的内存区域。如果未指定__restrict,则编译器将不会对循环进行向量化,或者仅在函数开头进行昂贵的运行时检查后才进行向量化,以确保数组确实不重叠。

此外,如果我们在没有fno-unroll-loops的情况下编译此示例并查看生成的循环,我们将看到编译器展开了向量化循环,该循环现在每次处理16个元素。

.LBB0_4:                                # =>This Inner Loop Header: Depth=1vmovdqu  (%rdi,%rax,8), %ymm0vmovdqu  32(%rdi,%rax,8), %ymm1vmovdqu  64(%rdi,%rax,8), %ymm2vmovdqu  96(%rdi,%rax,8), %ymm3vpaddq  (%rsi,%rax,8), %ymm0, %ymm0vpaddq  32(%rsi,%rax,8), %ymm1, %ymm1vpaddq  64(%rsi,%rax,8), %ymm2, %ymm2vpaddq  96(%rsi,%rax,8), %ymm3, %ymm3vmovdqu  %ymm0, (%rdx,%rax,8)vmovdqu  %ymm1, 32(%rdx,%rax,8)vmovdqu  %ymm2, 64(%rdx,%rax,8)vmovdqu  %ymm3, 96(%rdx,%rax,8)addq  $16, %raxcmpq  %rax, %r8jne  .LBB0_4

有一个非常有用的工具,可以帮助您识别编译器在哪些地方执行或不执行矢量化以避免汇编检查。您可以向clang添加-Rpass=loop-vectorize-Rpass-missed=loop-vectorize-Rpass-analysis=loop-vectorize选项。gcc也有类似的选项。

如果我们使用这些选项编译我们的示例,将会得到以下输出:

$ /usr/bin/clang++-15 -mavx2 -Rpass=loop-vectorize -Rpass-missed=loop-vectorize -Rpass-analysis=loop-vectorize -O3vectorization_example.cpp:7:5: remark: vectorized loop (vectorization width: 4, interleaved count: 4) [-Rpass=loop-vectorize]for (size_t i = 0; i < size; ++i) {

现在来看另外一个例子:

class SumFunction
{
public:void sumIf(int64_t * values, int8_t * filter, size_t size);int64_t sum = 0;
};void SumFunction::sumIf(int64_t * values, int8_t * filter, size_t size)
{for (size_t i = 0; i < size; ++i) {sum += filter[i] ? 0 : values[i];}
}
/usr/bin/clang++-15 -mavx2 -O3 -Rpass-analysis=loop-vectorize -Rpass=loop-vectorize -Rpass-missed=loop-vectorize -c vectorization_example.cpp...vectorization_example.cpp:28:9: remark: loop not vectorized [-Rpass-missed=loop-vectorize]for (size_t i = 0; i < size; ++i) {

在编译器无法执行矢量化的情况下,有两种可能的情况:

1. 您可以尝试修改代码,以便进行矢量化。在一些复杂的情况下,您可能需要重新设计数据表示。我强烈鼓励您查阅LLVM文档和gcc文档,这可以帮助您了解何时可以或不能执行自动矢量化的情况。

2. 您可以使用内部函数手动矢量化循环。由于需要额外的维护,这个选项不太受欢迎。

为了修复我们示例中的问题,我们需要在函数内部进行本地求和:

class SumFunction
{
public:void sumIf(int64_t * values, int8_t * filter, size_t size);int64_t sum = 0;
};void SumFunction::sumIf(int64_t * values, int8_t * filter, size_t size)
{int64_t local_sum = 0;for (size_t i = 0; i < size; ++i) {local_sum += filter[i] ? 0 : values[i];}sum += local_sum;
}

这样的代码示例被编译器矢量化:

/usr/bin/clang++-15 -mavx2 -O3 -Rpass-analysis=loop-vectorize -Rpass=loop-vectorize -Rpass-missed=loop-vectorize -c vectorization_example.cppvectorization_example.cpp:31:5: remark: vectorized loop (vectorization width: 4, interleaved count: 4) [-Rpass=loop-vectorize]for (size_t i = 0; i < size; ++i) {

在生成的汇编中,矢量化循环如下:

.LBB0_5:                                # =>This Inner Loop Header: Depth=1vmovd  (%rdx,%rax), %xmm5              # xmm5 = mem[0],zero,zero,zerovmovd  4(%rdx,%rax), %xmm6             # xmm6 = mem[0],zero,zero,zerovmovd  8(%rdx,%rax), %xmm7             # xmm7 = mem[0],zero,zero,zerovmovd  12(%rdx,%rax), %xmm1            # xmm1 = mem[0],zero,zero,zerovpcmpeqb  %xmm5, %xmm8, %xmm5vpmovsxbq  %xmm5, %ymm5vpcmpeqb  %xmm6, %xmm8, %xmm6vpmovsxbq  %xmm6, %ymm6vpcmpeqb  %xmm7, %xmm8, %xmm7vpmovsxbq  %xmm7, %ymm7vpcmpeqb  %xmm1, %xmm8, %xmm1vpmaskmovq  -96(%r8,%rax,8), %ymm5, %ymm5vpmovsxbq  %xmm1, %ymm1vpmaskmovq  -64(%r8,%rax,8), %ymm6, %ymm6vpaddq  %ymm0, %ymm5, %ymm0vpmaskmovq  -32(%r8,%rax,8), %ymm7, %ymm5vpaddq  %ymm2, %ymm6, %ymm2vpmaskmovq  (%r8,%rax,8), %ymm1, %ymm1vpaddq  %ymm3, %ymm5, %ymm3vpaddq  %ymm4, %ymm1, %ymm4addq  $16, %raxcmpq  %rax, %r9jne  .LBB0_5

CPU调度基础知识

CPU调度是一种技术,当有多个针对不同CPU特性的编译版本时,在运行时,程序会检测您的计算机具有哪些CPU特性,并在运行时使用性能最佳的版本。您想要检查的最重要的指令集是SSE4.2、AVX、AVX2和AVX-512。

要实现CPU调度,首先,我们需要使用CPUID指令来检查当前CPU是否支持特定的特性。

您可以使用内联汇编调用cpuid指令,也可以使用定义了这些函数的cpuid.h头文件:

/* x86-64 uses %rbx as the base register, so preserve it. */
#define __cpuid(__leaf, __eax, __ebx, __ecx, __edx) \__asm("  xchgq  %%rbx,%q1\n" \"  cpuid\n" \"  xchgq  %%rbx,%q1" \: "=a"(__eax), "=r" (__ebx), "=c"(__ecx), "=d"(__edx) \: "0"(__leaf))#define __cpuid_count(__leaf, __count, __eax, __ebx, __ecx, __edx) \__asm("  xchgq  %%rbx,%q1\n" \"  cpuid\n" \"  xchgq  %%rbx,%q1" \: "=a"(__eax), "=r" (__ebx), "=c"(__ecx), "=d"(__edx) \: "0"(__leaf), "2"(__count))
#endif

接下来,要检查某个CPU特性是否受支持,您需要检查Intel软件优化参考手册第5章手册的具体指令。例如,对于SSE4.2:

bool hasSSE42()
{uint32_t eax = 0;uint32_t ebx = 0;uint32_t ecx = 0;uint32_t edx = 0;__cpuid(0x1, eax, ebx, ecx, edx);return (ecx >> 20) & 1ul;
}

现在,我们需要使用不同的指令编译我们的函数。在clang中,有一个目标属性可以做到这一点。在gcc中,也有相同的属性。例如:

void plusDefault(int64_t * __restrict a, int64_t * __restrict b, int64_t * __restrict c, size_t size)
{for (size_t i = 0; i < size; ++i) {c[i] = a[i] + b[i];}
}__attribute__((target("sse,sse2,sse3,ssse3,sse4,avx,avx2")))
void plusAVX2(int64_t * __restrict a, int64_t * __restrict b, int64_t * __restrict c, size_t size)
{for (size_t i = 0; i < size; ++i) {c[i] = a[i] + b[i];}
}__attribute__((target("sse,sse2,sse3,ssse3,sse4,avx,avx2,avx512f")))
void plusAVX512(int64_t * __restrict a, int64_t * __restrict b, int64_t * __restrict c, size_t size)
{for (size_t i = 0; i < size; ++i) {c[i] = a[i] + b[i];}
}

在这个例子中,我们为AVX2和AVX-512额外编译了我们的plus函数。在最终的汇编中,我们可以检查编译器是否使用AVX2来矢量化plusAVX2函数的循环:

....globl  _Z8plusAVX2PlS_S_m              # -- Begin function _Z8plusAVX2PlS_S_m....LBB4_4:                                # =>This Inner Loop Header: Depth=1vmovdqu  (%rsi,%rax,8), %ymm0vmovdqu  32(%rsi,%rax,8), %ymm1vmovdqu  64(%rsi,%rax,8), %ymm2vmovdqu  96(%rsi,%rax,8), %ymm3vpaddq  (%rdi,%rax,8), %ymm0, %ymm0vpaddq  32(%rdi,%rax,8), %ymm1, %ymm1vpaddq  64(%rdi,%rax,8), %ymm2, %ymm2vpaddq  96(%rdi,%rax,8), %ymm3, %ymm3vmovdqu  %ymm0, (%rdx,%rax,8)vmovdqu  %ymm1, 32(%rdx,%rax,8)vmovdqu  %ymm2, 64(%rdx,%rax,8)vmovdqu  %ymm3, 96(%rdx,%rax,8)addq  $16, %raxcmpq  %rax, %r8jne  .LBB4_4...

以及使用AVX-512来矢量化plusAVX512循环:

....globl  _Z10plusAVX512PlS_S_m    # -- Begin function _Z10plusAVX512PlS_S_m....LBB5_4:    # =>This Inner Loop Header: Depth=1vmovdqu64  (%rsi,%rax,8), %zmm0vmovdqu64  64(%rsi,%rax,8), %zmm1vmovdqu64  128(%rsi,%rax,8), %zmm2vmovdqu64  192(%rsi,%rax,8), %zmm3vpaddq  (%rdi,%rax,8), %zmm0, %zmm0vpaddq  64(%rdi,%rax,8), %zmm1, %zmm1vpaddq  128(%rdi,%rax,8), %zmm2, %zmm2vpaddq  192(%rdi,%rax,8), %zmm3, %zmm3vmovdqu64  %zmm0, (%rdx,%rax,8)vmovdqu64  %zmm1, 64(%rdx,%rax,8)vmovdqu64  %zmm2, 128(%rdx,%rax,8)vmovdqu64  %zmm3, 192(%rdx,%rax,8)addq  $32, %raxcmpq  %rax, %r8jne  .LBB5_4...

现在我们已经有了执行CPU调度所需的一切:

void plus(int64_t * __restrict a, int64_t * __restrict b, int64_t * __restrict c, size_t size)
{if (hasAVX512()) {plusAVX512(a, b, c, size);} else if (hasAVX2()) {plusAVX2(a, b, c, size);} else {plusDefault(a, b, c, size);}
}

在这个例子中,我们创建了一个plus函数,它根据可用的指令集调度到具体的实现。这种CPU调度方法也被称为每次调用都进行调度。还有其他一些方法,您可以在Agner Fog的《在C++中优化软件:Windows、Linux和Mac平台的优化指南》第13.1节“CPU调度策略”中了解更多信息(https://www.agner.org/optimize/)。

每次调用都是最灵活的方法,因为它允许您使用模板函数和类成员函数,或根据运行时收集的一些统计数据选择一个实现。唯一的缺点是分支,尽管这种开销在函数执行大量工作时是可以忽略的。

CPU调度优化位置

现在要找到可以应用SIMD优化的地方?

  1. 如果您知道程序中哪些循环很热门,可以尝试为它们应用CPU调度。

  2. 如果您进行性能测试,可以使用AVX、AVX2、AVX-512编译您的程序,并比较性能报告,以找出使用CPU调度可以优化的程序中的位置。

这种带有性能测试的技术不仅适用于CPU调度,还适用于许多其他有用的优化。主要思想是使用不同的配置(编译器、编译器选项、库、分配器)编译代码,如果某些地方有性能提升,您可以手动优化它们。例如:

  1. 尝试不同的分配器和不同的库。

  2. 尝试不同的编译器选项(循环展开、内联阈值)。

  3. 为构建启用AVX/AVX2/AVX-512。

ClickHouse中的CPU调度

我们要感谢Dmitriy Kovalkov为ClickHouse添加了CPU调度框架。它成为本文描述的后续工作的基础。

首先,我想展示一下我们在ClickHouse中如何设计我们的调度框架。

enum class TargetArch : UInt32
{Default  = 0,         /// Without any additional compiler options.SSE42    = (1 << 0),  /// SSE4.2AVX      = (1 << 1),AVX2     = (1 << 2),AVX512F  = (1 << 3),AVX512BW    = (1 << 4),AVX512VBMI  = (1 << 5),AVX512VBMI2 = (1 << 6),
};/// Runtime detection.
bool isArchSupported(TargetArch arch);

我们为目标体系结构定义了一个enum TargetArch,并在isArchSupported函数中使用我们已经讨论过的CPUID指令集检查。然后,我们定义了一堆BEGIN_INSTRUCTION_SET_SPECIFIC_CODE部分,将目标属性应用于整个代码块。

例如,对于clang:

#   define BEGIN_AVX512F_SPECIFIC_CODE \
_Pragma("clang attribute push(__attribute__((target(\"sse,sse2,sse3,ssse3,sse4,\popcnt,avx,avx2, avx512f\"))), apply_to=function)")
\
#   define BEGIN_AVX2_SPECIFIC_CODE \
_Pragma("clang attribute push(__attribute__((target(\"sse,sse2,sse3,ssse3,sse4,\popcnt, avx,avx2\"))), apply_to=function)") \
\
#   define END_TARGET_SPECIFIC_CODE \
_Pragma("clang attribute pop")

然后,对于每个指令集,我们定义了一个单独的命名空间TargetSpecific::INSTRUCTION_SET。AVX2和AVX512的示例:

#define DECLARE_AVX2_SPECIFIC_CODE(...) \
BEGIN_AVX2_SPECIFIC_CODE \
namespace TargetSpecific::AVX2 { \DUMMY_FUNCTION_DEFINITION \using namespace DB::TargetSpecific::AVX2; \__VA_ARGS__ \
} \
END_TARGET_SPECIFIC_CODE#define DECLARE_AVX512F_SPECIFIC_CODE(...) \
BEGIN_AVX512F_SPECIFIC_CODE \
namespace TargetSpecific::AVX512F { \DUMMY_FUNCTION_DEFINITION \using namespace DB::TargetSpecific::AVX512F; \__VA_ARGS__ \
} \
END_TARGET_SPECIFIC_CODE

它可以这样使用:

DECLARE_DEFAULT_CODE (int funcImpl() {return 1;}
) // DECLARE_DEFAULT_CODEDECLARE_AVX2_SPECIFIC_CODE (int funcImpl() {return 2;}
) // DECLARE_AVX2_SPECIFIC_CODE/// Dispatcher function
int dispatchFunc() {
#if USE_MULTITARGET_CODEif (isArchSupported(TargetArch::AVX2))return TargetSpecific::AVX2::funcImpl();
#endifreturn TargetSpecific::Default::funcImpl();
}

上面的示例在独立函数中运作良好,但是当我们有类成员函数时,它们不起作用,因为这些函数不能包装到命名空间中。对于这种情况,我们有另一堆宏。我们需要在类成员函数名之前插入一个特定的属性,并生成带有不同名称的函数,最好带有后缀,如SSE42、AVX2、AVX512。我们可以使用MULTITARGET_FUNCTION_HEADERMULTITARGET_FUNCTION_BODY宏将函数拆分为头部和主体。然后在函数名之前插入特定的属性。例如,对于AVX-512(BW)、AVX-512(F)、AVX2和SSE4.2,可以是这样:

/// Function header
#define MULTITARGET_FUNCTION_HEADER(...) __VA_ARGS__/// Function body
#define MULTITARGET_FUNCTION_BODY(...) __VA_ARGS__#define MULTITARGET_FUNCTION_AVX512BW_AVX512F_AVX2_SSE42(FUNCTION_HEADER, name, FUNCTION_BODY) \FUNCTION_HEADER \\AVX512BW_FUNCTION_SPECIFIC_ATTRIBUTE \name##AVX512BW \FUNCTION_BODY \\FUNCTION_HEADER \\AVX512_FUNCTION_SPECIFIC_ATTRIBUTE \name##AVX512 \FUNCTION_BODY \\FUNCTION_HEADER \\AVX2_FUNCTION_SPECIFIC_ATTRIBUTE \name##AVX2 \FUNCTION_BODY \\FUNCTION_HEADER \\SSE42_FUNCTION_SPECIFIC_ATTRIBUTE \name##SSE42 \FUNCTION_BODY \\FUNCTION_HEADER \\name \FUNCTION_BODY \

我们在需要进行大量计算的地方使用CPU调度,例如在哈希、几何函数、字符串处理函数、随机数生成函数、一元函数和聚合函数中。例如,让我们看看如何在聚合函数中使用CPU调度。在ClickHouse中,如果存在没有键的GROUP BY,例如SELECT sum(value)avg(value) FROM test_table,聚合函数直接以批处理方式处理数据。对于sum函数,有以下实现:

template <typename Value>
void NO_INLINE addManyImpl(const Value * __restrict ptr, size_t start, size_t end)
{ptr += start;size_t count = end - start;const auto * end_ptr = ptr + count;/// LoopT local_sum{};while (ptr < end_ptr){Impl::add(local_sum, *ptr);++ptr;}Impl::add(sum, local_sum);
}

在我们将此循环包装到我们的调度框架中后,函数代码将如下所示:

MULTITARGET_FUNCTION_AVX512BW_AVX512F_AVX2_SSE42(
MULTITARGET_FUNCTION_HEADER(
template <typename Value>
void NO_SANITIZE_UNDEFINED NO_INLINE
), addManyImpl,
MULTITARGET_FUNCTION_BODY((const Value * __restrict ptr, size_t start, size_t end)
{ptr += start;size_t count = end - start;const auto * end_ptr = ptr + count;/// LoopT local_sum{};while (ptr &lt end_ptr){Impl::add(local_sum, *ptr);++ptr;}Impl::add(sum, local_sum);
}))

现在,我们可以根据最快的可用CPU指令集调度到正确的实现:

template <typename Value>
void NO_INLINE addMany(const Value * __restrict ptr, size_t start, size_t end)
{
#if USE_MULTITARGET_CODEif (isArchSupported(TargetArch::AVX512BW)){addManyImplAVX512BW(ptr, start, end);return;} else if (isArchSupported(TargetArch::AVX512F)){addManyImplAVX512F(ptr, start, end);return;}else if (isArchSupported(TargetArch::AVX2)){addManyImplAVX2(ptr, start, end);return;}else if (isArchSupported(TargetArch::SSE42)){addManyImplSSE42(ptr, start, end);return;}
#endifaddManyImpl(ptr, start, end);
}

在应用此优化后,性能报告的一小部分如下:

QueryOld (s)New (s)

Ratio of

speedup(-) or slowdown(+)

Relative 

difference (new - old) / old

SELECT sum(toNullable(toUInt8(number))) FROM numbers(100000000)0.1100.077-1.428x-0.300
SELECT sum(number) FROM numbers(100000000)0.0440.035-1.228x-0.185
SELECT sumOrNull(number) FROM numbers(100000000)0.0440.036-1.226x-0.183
SELECT avg(number) FROM numbers(100000000)0.4160.341-1.219x-0.180

总体而言,对于sum和avg聚合函数的这种优化将性能提高了1.2-1.6倍。类似的优化也可以应用于其他聚合函数。现在让我们看一下一元函数中的CPU调度优化:

template <typename A, typename Op>
struct UnaryOperationImpl
{using ResultType = typename Op::ResultType;using ColVecA = ColumnVectorOrDecimal<A>;using ColVecC = ColumnVectorOrDecimal<ResultType>using ArrayA = typename ColVecA::Container;using ArrayC = typename ColVecC::Container;static void vector(const ArrayA & a, ArrayC & c)
{/// Loop Op::apply is template for operationsize_t size = a.size();for (size_t i = 0; i < size; ++i)c[i] = Op::apply(a[i]);}static void constant(A a, ResultType & c)
{c = Op::apply(a);}
};

在示例中,有一个循环,它使用Op::apply对数组a的元素应用一些模板操作,并将结果写入数组c。在我们将此循环包装到我们的调度框架中后,循环代码将如下所示:

MULTITARGET_FUNCTION_WRAPPER_AVX2_SSE42(
MULTITARGET_FH(static void NO_INLINE),
vectorImpl,
MULTITARGET_FB((const ArrayA & a, ArrayC & c) /// NOLINT
{/// Loop Op::apply is template for operationsize_t size = a.size();for (size_t i = 0; i < size; ++i)c[i] = Op::apply(a[i]);
}))

现在,我们需要根据当前可用的CPU指令集调度到适当的函数:

static void NO_INLINE vector(const ArrayA & a, ArrayC & c)
{
#if USE_MULTITARGET_CODEif (isArchSupported(TargetArch::AVX2)){vectorImplAVX2(a, c);return;}else if (isArchSupported(TargetArch::SSE42)){vectorImplSSE42(a, c);return;}
#endifvectorImpl(a, c);
}

在应用此优化后,性能报告的一小部分如下:

QueryOld (s)New (s)

Ratio of

speedup(-) or slowdown(+)

Relative difference (new - old) / old
SELECT roundDuration(toInt32(number))) FROM numbers(100000000)1.6320.229-7.119x-0.860
SELECT intExp2(toInt32(number)) FROM numbers(100000000)0.1480.105-1.413x-0.293
SELECT roundToExp2(toUInt8(number)) FROM numbers(100000000)0.1440.102-1.41x-0.291

总体而言,对于一元函数的这种优化将性能提高了1.15-2倍。对于一些特定函数,例如roundDuration,这样的优化提高了2-7倍的性能。

总结

编译器可以使用SIMD指令矢量化甚至复杂的循环。此外,您可以手动矢量化代码或设计面向SIMD的算法。但最大的问题是,如果要使用现代指令集,它可能会降低程序或库的可移植性。运行时CPU调度可以帮助您消除此问题,代价是为不同体系结构多次编译代码的部分。您可以通过性能测试找到提高性能的地方,并使用不同的配置编译代码。对于CPU调度优化,您可以使用AVX、AVX2、AVX512编译代码,并在性能有提升的地方手动应用CPU调度。在ClickHouse中,我们专门为此类优化设计了一个框架,并在许多地方提高了性能。

Meetup 活动报名通知

好消息:ClickHouse Shenzhen User Group第1届 Meetup 已经开放报名了,将于2024年1月6日在深圳南山区海天二路33号腾讯滨海大厦举行,扫码免费报名

图片

​​联系我们

手机号:13910395701

邮箱:Tracy.Wang@clickhouse.com

满足您所有的在线分析列式数据库管理需求

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

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

相关文章

《PySpark大数据分析实战》-16.云服务模式Databricks介绍运行案例

&#x1f4cb; 博主简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是wux_labs。&#x1f61c; 热衷于各种主流技术&#xff0c;热爱数据科学、机器学习、云计算、人工智能。 通过了TiDB数据库专员&#xff08;PCTA&#xff09;、TiDB数据库专家&#xff08;PCTP…

浅析RoPE旋转位置编码的远程衰减特性

为什么 θ i \theta_i θi​的取值会造成远程衰减性 旋转位置编码的出发点为&#xff1a;通过绝对位置编码的方式实现相对位置编码。 对词向量 q \boldsymbol{q} q添加绝对位置信息 m m m&#xff0c;希望找到一种函数 f f f&#xff0c;使得&#xff1a; < f ( q , m ) …

windows netstat命令

文章目录 前言各选项的含义如下&#xff1a; 前言 Netstat是控制台命令,是一个监控TCP/IP网络的非常有用的工具&#xff0c;它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。Netstat用于显示与IP、TCP、UDP和ICMP协议相关的统计数据&#xff0c;一般用于检…

Python中的复数

复数一般表示为abi(a、b为有理数)&#xff0c;在python中i被挪着它用&#xff0c;虚数单位是不区分大小写的J。 (笔记模板由python脚本于2023年12月19日 18:58:39创建&#xff0c;本篇笔记适合认识复数的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https:/…

AIGC:阿里开源大模型通义千问部署与实战

1 引言 通义千问-7B&#xff08;Qwen-7B&#xff09;是阿里云研发的通义千问大模型系列的70亿参数规模的模型。Qwen-7B是基于Transformer的大语言模型, 在超大规模的预训练数据上进行训练得到。预训练数据类型多样&#xff0c;覆盖广泛&#xff0c;包括大量网络文本、专业书籍…

机器学习算法--朴素贝叶斯(Naive Bayes)

实验环境 1. python3.7 2. numpy > 1.16.4 3. sklearn > 0.23.1 朴素贝叶斯的介绍 朴素贝叶斯算法&#xff08;Naive Bayes, NB) 是应用最为广泛的分类算法之一。它是基于贝叶斯定义和特征条件独立假设的分类器方法。NB模型所需估计的参数很少&#xff0c;对缺失数据不…

微信小程序动态导航栏(uniapp + vant)

本文使用到vant的van-tabbar组件来实现 一、uniapp整合vant ui vant小程序版本:https://vant-contrib.gitee.io/vant-weapp/#/home 注:vant并没有uniapp的版本,所以此处是引入小程序版本的ui 1. 下载vant编译后代码 https://github.com/youzan/vant-weapp/tree/dev/dist 2…

人工智能时代,看好硅光子!

硅光子学是一种用于制备光子集成电路&#xff08;PIC&#xff09;的技术&#xff0c;通常用于产生、检测、传输和处理光。这种方法使用半导体绝缘体上硅&#xff08;SOI&#xff09;晶片作为衬底材料&#xff0c;并采用标准的互补金属氧化物半导体&#xff08;CMOS&#xff09;…

【python】程序运行添加命令行参数argparse模块用法详解

Python标准库之argparse&#xff0c;详解如何创建一个ArgumentParser对象及使用 一. argparse介绍二. 使用步骤及参数介绍三. 具体使用3.1 设置必需参数3.2 传一个参数3.3 传多个参数3.4 位置参数和可选参数3.5 参数设置默认值3.6 其它用法 一. argparse介绍 很多时候&#xff…

第三节TypeScript 基础类型

1、typescript的基础类型 如下表&#xff1a; 数据类型 关键字 描述 任意类型 any 生命any的变量可以赋值任意类型的值 数字类型 number 整数或分数 字符串类型 string 使用单引号&#xff08;‘’&#xff09;或者双引号&#xff08;“”&#xff09;来表示字符串…

随时爆雷!2023年四大“安全债”

即将过去的2023年&#xff0c;网络安全、云安全、应用安全、数据安全领域暴露的诸多“安全债”中&#xff0c;有四大债务不但未能充分缓解&#xff0c;反而有在新的一年“爆雷”的风险。这四大债务分别是&#xff1a;Logj4漏洞、HTTP/2快速重置攻击漏洞、恶意电子邮件和后量子加…

智能优化算法应用:基于野狗算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于野狗算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于野狗算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.野狗算法4.实验参数设定5.算法结果6.参考文献7.MA…