You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Numba实现的popcount代码比等效C代码快一倍?

为什么Numba实现的popcount比等效C代码快一倍?

Numba实现代码

from numba import njit
import numba as nb

@nb.njit(nb.uint64(nb.uint64))
def popcount(x): 
      b=0
      while(x > 0):
          x &= x - nb.uint64(1)   
          b+=1
      return b

@njit
def timed_loop(n):
    summand = 0
    for i in range(n):
        summand += popcount(i)
    return summand

该代码用于累加从0到n-1所有整数的popcount(置位位数)。

Numba性能测试结果

%timeit timed_loop(1000000)
340 µs ± 1.08 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

LLVM会将该popcount函数智能转换为CPU原生POPCNT指令,理论性能优异。

对比用的C代码

#include <stdio.h>
#include <time.h>

// Function to calculate the population count (number of set bits) of an integer using __builtin_popcount
int popcount(int num) {
    return __builtin_popcount(num);
}

int main() {
    unsigned int n;
    printf("Enter the value of n: ");
    scanf("%d", &n);

    // Variables to store start and end times
    struct timespec start_time, end_time;

    // Get the current time as the start time
    clock_gettime(CLOCK_MONOTONIC, &start_time);

    int sum = 0;
    for (unsigned int i = 0; i < n; i++) {
        sum += popcount(i);
    }

    // Get the current time as the end time
    clock_gettime(CLOCK_MONOTONIC, &end_time);

    // Calculate the elapsed time in microseconds
    long long elapsed_time = (end_time.tv_sec - start_time.tv_sec) * 1000000LL +
                            (end_time.tv_nsec - start_time.tv_nsec) / 1000;

    printf("Sum of population counts from 0 to %d-1 is: %d\n", n, sum);
    printf("Elapsed time: %lld microseconds\n", elapsed_time);

    return 0;
}

使用-march=native -Ofast编译,gcc和clang结果相近:

./popcount 
Enter the value of n: 1000000
Sum of population counts from 0 to 1000000-1 is: 9884992
Elapsed time: 732 microseconds

核心原因分析

1. 数据类型宽度差异

Numba代码全程使用64位无符号整数(uint64),而C代码采用32位整数(int/unsigned int)。虽然POPCNT指令对32位、64位操作的延迟相近,但LLVM(Numba后端)对64位数据的循环向量化效率更高——它可利用AVX2等256位SIMD寄存器一次处理4个64位整数,累加操作的指令吞吐量更优,减少了循环迭代次数与指令调度开销。

2. 编译器优化策略差异

Numba使用的LLVM版本对这类简单循环的向量化、内联优化更激进:

  • popcount会被完全内联到timed_loop中,LLVM还会将popcount与累加操作合并为紧凑的指令序列,避免额外寄存器移动开销。
  • GCC/Clang在-Ofast下虽也会内联__builtin_popcount,但对32位累加循环的向量化策略相对保守,未充分利用SIMD并行累加能力。

3. 类型匹配减少隐式开销

Numba中循环变量i、累加器summand与popcount的输入输出类型完全匹配(均为uint64),无类型转换开销;而C代码中循环变量是unsigned int、累加器是int,每次累加都存在隐式类型扩展,积少成多后拉大了性能差距。

4. 计时方式的细微影响

%timeit会自动预热JIT代码并多次运行取平均,排除单次波动;而C代码是单次运行计时,若系统存在临时负载,单次计时可能偏高(但这不是差距的主要原因)。

验证优化方法

修改C代码为64位类型并优化编译参数,可缩小性能差距:

#include <stdio.h>
#include <time.h>
#include <stdint.h>

uint64_t popcount(uint64_t num) {
    return __builtin_popcountll(num);
}

int main() {
    uint64_t n;
    printf("Enter the value of n: ");
    scanf("%lu", &n);

    struct timespec start_time, end_time;
    clock_gettime(CLOCK_MONOTONIC, &start_time);

    uint64_t sum = 0;
    for (uint64_t i = 0; i < n; i++) {
        sum += popcount(i);
    }

    clock_gettime(CLOCK_MONOTONIC, &end_time);

    long long elapsed_time = (end_time.tv_sec - start_time.tv_sec) * 1000000LL +
                            (end_time.tv_nsec - start_time.tv_nsec) / 1000;

    printf("Sum of population counts from 0 to %lu-1 is: %lu\n", n, sum);
    printf("Elapsed time: %lld microseconds\n", elapsed_time);

    return 0;
}

用gcc -march=native -Ofast -o popcount popcount.c编译后,性能会接近Numba的测试结果。

内容的提问来源于stack exchange,提问作者Simd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 23:33:30