ClickHouse中的CPU调度

news2024/11/15 8:22:50

图片

本文字数: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, %rcx
  je  .LBB0_7
# %bb.1:
  cmpq  $4, %rcx
  jae  .LBB0_3
# %bb.2:
  xorl  %r8d, %r8d
  jmp  .LBB0_6
.LBB0_3:
  movq  %rcx, %r8
  andq  $-4, %r8
  xorl  %eax, %eax
  .p2align  4, 0x90
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
  vmovdqu  (%rdi,%rax,8), %ymm0
  vpaddq  (%rsi,%rax,8), %ymm0, %ymm0
  vmovdqu  %ymm0, (%rdx,%rax,8)
  addq  $4, %rax
  cmpq  %rax, %r8
  jne  .LBB0_4
# %bb.5:
  cmpq  %rcx, %r8
  je  .LBB0_7
  .p2align  4, 0x90
.LBB0_6:                                # =>This Inner Loop Header: Depth=1
  movq  (%rdi,%r8,8), %rax
  addq  (%rsi,%r8,8), %rax
  movq  %rax, (%rdx,%r8,8)
  incq  %r8
  cmpq  %r8, %rcx
  jne  .LBB0_6
.LBB0_7:
  vzeroupper
  retq

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

.LBB0_4:                                # =>This Inner Loop Header: Depth=1
  vmovdqu  (%rdi,%rax,8), %ymm0
  vpaddq  (%rsi,%rax,8), %ymm0, %ymm0
  vmovdqu  %ymm0, (%rdx,%rax,8)
  addq  $4, %rax
  cmpq  %rax, %r8
  jne  .LBB0_4

标量循环:

.LBB0_6:                                # =>This Inner Loop Header: Depth=1
  movq  (%rdi,%r8,8), %rax
  addq  (%rsi,%r8,8), %rax
  movq  %rax, (%rdx,%r8,8)
  incq  %r8
  cmpq  %r8, %rcx
  jne  .LBB0_6

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

# %bb.1:
  cmpq  $4, %rcx
  jae  .LBB0_3
# %bb.2:
  xorl  %r8d, %r8d
  jmp  .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=1
  vmovdqu  (%rdi,%rax,8), %ymm0
  vmovdqu  32(%rdi,%rax,8), %ymm1
  vmovdqu  64(%rdi,%rax,8), %ymm2
  vmovdqu  96(%rdi,%rax,8), %ymm3
  vpaddq  (%rsi,%rax,8), %ymm0, %ymm0
  vpaddq  32(%rsi,%rax,8), %ymm1, %ymm1
  vpaddq  64(%rsi,%rax,8), %ymm2, %ymm2
  vpaddq  96(%rsi,%rax,8), %ymm3, %ymm3
  vmovdqu  %ymm0, (%rdx,%rax,8)
  vmovdqu  %ymm1, 32(%rdx,%rax,8)
  vmovdqu  %ymm2, 64(%rdx,%rax,8)
  vmovdqu  %ymm3, 96(%rdx,%rax,8)
  addq  $16, %rax
  cmpq  %rax, %r8
  jne  .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 -O3

vectorization_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.cpp

vectorization_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=1
  vmovd  (%rdx,%rax), %xmm5              # xmm5 = mem[0],zero,zero,zero
  vmovd  4(%rdx,%rax), %xmm6             # xmm6 = mem[0],zero,zero,zero
  vmovd  8(%rdx,%rax), %xmm7             # xmm7 = mem[0],zero,zero,zero
  vmovd  12(%rdx,%rax), %xmm1            # xmm1 = mem[0],zero,zero,zero
  vpcmpeqb  %xmm5, %xmm8, %xmm5
  vpmovsxbq  %xmm5, %ymm5
  vpcmpeqb  %xmm6, %xmm8, %xmm6
  vpmovsxbq  %xmm6, %ymm6
  vpcmpeqb  %xmm7, %xmm8, %xmm7
  vpmovsxbq  %xmm7, %ymm7
  vpcmpeqb  %xmm1, %xmm8, %xmm1
  vpmaskmovq  -96(%r8,%rax,8), %ymm5, %ymm5
  vpmovsxbq  %xmm1, %ymm1
  vpmaskmovq  -64(%r8,%rax,8), %ymm6, %ymm6
  vpaddq  %ymm0, %ymm5, %ymm0
  vpmaskmovq  -32(%r8,%rax,8), %ymm7, %ymm5
  vpaddq  %ymm2, %ymm6, %ymm2
  vpmaskmovq  (%r8,%rax,8), %ymm1, %ymm1
  vpaddq  %ymm3, %ymm5, %ymm3
  vpaddq  %ymm4, %ymm1, %ymm4
  addq  $16, %rax
  cmpq  %rax, %r9
  jne  .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=1
  vmovdqu  (%rsi,%rax,8), %ymm0
  vmovdqu  32(%rsi,%rax,8), %ymm1
  vmovdqu  64(%rsi,%rax,8), %ymm2
  vmovdqu  96(%rsi,%rax,8), %ymm3
  vpaddq  (%rdi,%rax,8), %ymm0, %ymm0
  vpaddq  32(%rdi,%rax,8), %ymm1, %ymm1
  vpaddq  64(%rdi,%rax,8), %ymm2, %ymm2
  vpaddq  96(%rdi,%rax,8), %ymm3, %ymm3
  vmovdqu  %ymm0, (%rdx,%rax,8)
  vmovdqu  %ymm1, 32(%rdx,%rax,8)
  vmovdqu  %ymm2, 64(%rdx,%rax,8)
  vmovdqu  %ymm3, 96(%rdx,%rax,8)
  addq  $16, %rax
  cmpq  %rax, %r8
  jne  .LBB4_4

...

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

...

.globl  _Z10plusAVX512PlS_S_m    # -- Begin function _Z10plusAVX512PlS_S_m

...

.LBB5_4:    # =>This Inner Loop Header: Depth=1
  vmovdqu64  (%rsi,%rax,8), %zmm0
  vmovdqu64  64(%rsi,%rax,8), %zmm1
  vmovdqu64  128(%rsi,%rax,8), %zmm2
  vmovdqu64  192(%rsi,%rax,8), %zmm3
  vpaddq  (%rdi,%rax,8), %zmm0, %zmm0
  vpaddq  64(%rdi,%rax,8), %zmm1, %zmm1
  vpaddq  128(%rdi,%rax,8), %zmm2, %zmm2
  vpaddq  192(%rdi,%rax,8), %zmm3, %zmm3
  vmovdqu64  %zmm0, (%rdx,%rax,8)
  vmovdqu64  %zmm1, 64(%rdx,%rax,8)
  vmovdqu64  %zmm2, 128(%rdx,%rax,8)
  vmovdqu64  %zmm3, 192(%rdx,%rax,8)
  addq  $32, %rax
  cmpq  %rax, %r8
  jne  .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.2
    AVX      = (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_CODE

DECLARE_AVX2_SPECIFIC_CODE (
    int funcImpl() {
        return 2;
    }
) // DECLARE_AVX2_SPECIFIC_CODE

/// Dispatcher function
int dispatchFunc() {
#if USE_MULTITARGET_CODE
    if (isArchSupported(TargetArch::AVX2))
        return TargetSpecific::AVX2::funcImpl();
#endif
    return 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;

    /// Loop
    T 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;

    /// Loop
    T 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_CODE
 if (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;
    }
#endif

    addManyImpl(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 operation
        size_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 operation
    size_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_CODE
    if (isArchSupported(TargetArch::AVX2))
    {
        vectorImplAVX2(a, c);
        return;
    }
    else if (isArchSupported(TargetArch::SSE42))
    {
        vectorImplSSE42(a, c);
        return;
    }
#endif

    vectorImpl(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.coloradmin.cn/o/1324848.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ASP.NET MVC权限管理系实战之一验证码功能实现

1&#xff0c;权限的管理系统&#xff1a;开发项目必备的一个的功能&#xff1b;该项目使用 ASP.NET MVC5 SqlServer EF6 IOC容器 BoostStrap 2&#xff0c;登录界面验证码功能实现&#xff0c;整体效果如下&#xff1b; 3&#xff0c;接下来就是代码部分实现&#xff0c;前端…

C#使用HTTP方式对接WebService

C#使用HTTP方式对接WebService C#对接WebService的几种方式 1.直接引用服务 添加服务 添加成功后, 会显示服务详细 调用服务 使用HTTPPost调用WebService option.RequestDataStr GetHttpRequestXml(strXmlBody); // 创建一个 HttpClient 对象 using (HttpClient client …

vba中字典的应用实例

vba中熟练使用字典可以帮我们解决很多问题&#xff0c;以下为字典的应用方法及案例&#xff1a; Sub dictionary() Dim d As New dictionary 定义字典 Dim mykey As Variant Dim myitems d.Add "1100000", "身份证" 字典录入key关键字和item条目 d.Add &q…

数字化技术助力英语习得 iEnglish成智慧化学习新选择

日前,美剧《老友记》中钱德勒的扮演者马修派瑞去世的消息引发不少人的回忆杀。《老友记》官方发文悼念马修派瑞:“对于马修派瑞去世的消息,我们深感悲痛,他是给我们所有人的真正礼物,我们的心和他的家人、爱人、所有的粉丝在一起。” 作为不少国人刷剧学习英语的首选,《老友记…

排序算法——快排

快速排序算法最早是由图灵奖获得者Tony Hoare设计出来的,他在形式化方法理论以 及ALGOL.60编程语言的发明中都有卓越的贡献,是20世纪最伟大的计算机科学家之—。 而这快速排序算法只是他众多贡献中的—个小发明而已。 快速排序&#xff08;Quick Sort&#xff09;的基本算法思…

1_js基本简介数据类型变量的使用

1. 编程语言简介 1.1 计算机编程语言 计算机编程语言是程序设计的最重要的工具&#xff0c;它是指计算机能够接受和处理的、具有一定语法规则的语言。从计算机诞生&#xff0c;计算机语言经历了机器语言、汇编语言和高级语言几个阶段。 高级语言&#xff1a;JavaScript&#x…

vue打包内存问题解决办法<--- Last few GCs ---><--- JS stacktrace --->

**<— Last few GCs —> [18484:0000026763669610] 106760 ms: Mark-sweep 4016.0 <— JS stacktrace —> FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory** 解决办法&#xff1a; set NODE_OPTION…

PaddleOCR Docker 容器快捷调用,快捷调用OCR API

文章目录 搞环境命令行测试Python调用测试转fastapi服务打包成镜像服务快速启动paddleOCR paddleOCR迎来大更新&#xff0c;搞一把新的api接口&#xff0c;直接用起来。 搞环境 搞容器&#xff1a; FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 ENV DEBIAN_FRONTENDno…

CUDA 学习记录2

1.是否启用一级缓存有什么影响&#xff1a; 启用一级缓存&#xff08;缓存加载操作经过一级缓存&#xff09;&#xff1a;一次内存十五操作以128字节的粒度进行。 不启用一级缓存&#xff08;没有缓存的加载不经过一级缓存&#xff09;&#xff1a;在内存段的粒度上&#xff…

条款6:若不想使用编译器自动生成的函数,就该明确拒绝

有些场景我们不需要编译器默认实现的构造函数&#xff0c;拷贝构造函数&#xff0c;赋值函数&#xff0c;这时候我们应该明确的告诉编译器&#xff0c;我们不需要&#xff0c;一个可行的方法是将拷贝构造函数和赋值函数声明为private。 class HomeForSale { ... }; HomeForSal…

故障排查:shell脚本输出乱码

博客主页&#xff1a;https://tomcat.blog.csdn.net 博主昵称&#xff1a;农民工老王 主要领域&#xff1a;Java、Linux、K8S 期待大家的关注&#x1f496;点赞&#x1f44d;收藏⭐留言&#x1f4ac; 目录 故障详情故障原因解决方法iconv命令介绍 故障详情 最近的工作中遇到一…

Python Opencv实践 - 手势音量控制

本文基于前面的手部跟踪功能做一个手势音量控制功能&#xff0c;代码用到了前面手部跟踪封装的HandDetector.这篇文章在这里&#xff1a; Python Opencv实践 - 手部跟踪-CSDN博客文章浏览阅读626次&#xff0c;点赞11次&#xff0c;收藏7次。使用mediapipe库做手部的实时跟踪&…

Linux静态ip

Linux静态ip Ⅰ、修改静态ip Ⅰ、修改静态ip 修改静态ip必须是root用户 su root //切换root用户 ip a //查看修改前的动态ipvi /etc/sysconfig/network-scripts/ifcfg-ens33 //打开网卡配置文件&#xff0c;修改一处&#xff0c;新增四处 BOOTPROTO&quo…

[Realtek sdk-3.4.14b]RTL8197FH-VG+RTL8812F WiFi使用功率限制功能使用说明

sdk说明 ** Gateway/AP firmware v3.4.14b – Aug 26, 2019**  Wireless LAN driver changes as:  Refine WiFi Stability and Performance  Add 8812F MU-MIMO  Add 97G/8812F multiple mac-clone  Add 97G 2T3R antenna diversity  Fix 97G/8812F/8814B MP issu…

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

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

MongoDB的原子操作findAndModify和findOneAndUpdate

本文主要介绍MongoDB的原子操作findAndModify和findOneAndUpdate。 目录 MongoDB的原子操作一、findAndModify二、findOneAndUpdate MongoDB的原子操作 MongoDB的原子操作指的是在单个操作中对数据库的数据进行读取和修改&#xff0c;并确保操作是原子的&#xff0c;即要么完全…

轮滑加盟培训机构管理系统源码开发方案

一、项目背景与目标 &#xff08;一&#xff09;项目背景 随着轮滑运动的普及和市场需求的增加&#xff0c;轮滑加盟培训机构逐渐兴起。这些机构面临着学员管理、课程排班、教师管理等多方面的挑战。为了提高管理效率和服务质量&#xff0c;需要开发一套专门针对轮滑加盟培训…

[总线仲裁]

目录 一. 集中仲裁方式1.1 链式查询方式1.2 计数器查询方式1.3 独立请求方式 二. 分布式仲裁方式 总线仲裁是为了解决多个设备争用总线这个问题 \quad 一. 集中仲裁方式 \quad 集中仲裁方式: 就像是霸道总裁来决定谁先获得总线控制权 分布仲裁方式: 商量着谁先获得总线控制权 …

SQL学习笔记+MySQL+SQLyog工具教程

文章目录 1、前言2、SQL基本语言及其操作2.1、CREATE TABLE – 创建表2.2、DROP TABLE – 删除表2.3、INSERT – 插入数据2.4、SELECT – 查询数据2.5、SELECTDISTINCT – 去除重复值后查询数据2.6、SELECTWHERE – 条件过滤2.7、AND & OR – 运算符2.8、ORDER BY – 排序2…

Linux:终端定时自动注销

这样防止了&#xff0c;当我们临时离开电脑这个空隙&#xff0c;被坏蛋给趁虚而入 定几十秒或者分钟&#xff0c;如果这个时间段没有输入东西那么就会自动退出 全局生效 这个系统中的所有用户生效 vim /etc/profile在末尾加入TMOUT10 TMOUT10 这个就是10 秒&#xff0c;按…