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

基于二进制浮点数的Radix Sort实现:内存释放疑问与性能优化问询

针对浮点数基数排序的问题解答

作业背景概述

我实现了一款针对二进制浮点数的Radix Sort算法,目标是在2分钟内完成1亿个浮点数的排序。目前算法效率较高,能在约30秒内完成任务。为了对浮点数进行位运算,我参考Quake 3快速逆平方根算法相关视频,将浮点数转换为unsigned int处理。

以下是核心排序代码:

void radixsort(float *array, unsigned int length, unsigned int bits) {
    double sum = 0;
    // Create buckets
    float *bucketsA = (float *) malloc(length * sizeof(float));
    float *bucketsB = (float *) malloc(length * sizeof(float));
    unsigned int aIndex = 0, bIndex = 0;

    for (int i = 0; i < bits; i++) {
        // Sort array using digit position d as the key.
        for (int j = 0; j < length; j++) {
            if (i == 0) sum += array[j];
            unsigned int conversion = * (unsigned int *) &array[j];
            int positionBit = nthBit(conversion, i);
            if (positionBit == 0) {
                bucketsA[aIndex] = array[j];
                aIndex++;
            } else {
                bucketsB[bIndex] = array[j];
                bIndex++;
            }
        }
        // Combine and move sorted buckets into original array
        if(i == bits - 1) {
            reverseArray(bucketsB, bIndex);
            memcpy(array, bucketsB, (bIndex + 1) * sizeof(float));
            memcpy(array + bIndex, bucketsA, (aIndex + 1) * sizeof(float));
        } else {
            memcpy(array, bucketsA, (aIndex + 1) * sizeof(float));
            memcpy(array + aIndex, bucketsB, (bIndex + 1) * sizeof(float));
        }
        // Reset the memory of the buckets
        memset(&bucketsA[0], 0, sizeof(float) * length);
        memset(&bucketsB[0], 0, sizeof(float) * length);
        // Reset bucket index
        aIndex = bIndex = 0;
    }
    printf("Total: %f\n", sum);
    // Free reserved memory
    free(bucketsA);
    free(bucketsB);
}

补充的辅助函数代码:

int nthBit(int number, int n) {
    return (number >> n) & 1;
}

void reverseArray(float *array, int size) {
    for (int i = 0; i < (size / 2); i++) {
        float swap = array[size - 1 - i];
        array[size - 1 - i] = array[i];
        array[i] = swap;
    }
}

问题解答

1. 未释放堆内存为何没有触发段错误?释放内存反而拖慢速度?

首先,未释放堆内存不会直接导致段错误。段错误通常是因为访问了不属于你的内存(比如空指针、越界访问、栈溢出等),而内存泄漏(未释放堆内存)只是让进程占用的内存没有归还给操作系统,程序本身依然可以正常运行——直到系统内存耗尽才会出问题,但单个排序程序的泄漏在短时间内不会触发这个情况。

至于释放内存拖慢速度:free()本身确实有一定开销,尤其是当你释放大块内存时,操作系统需要把内存块重新加入空闲链表,做一些管理工作。如果你的程序在radixsort返回后就退出了,其实操作系统会自动回收进程的所有内存,这时候手动调用free()反而多了一层不必要的操作。不过在长期运行的程序中,内存泄漏会导致内存占用持续增长,这时候free()是必须的,但你的排序程序属于一次性任务,确实可以考虑省略(或者在性能优先的场景下暂时跳过)。

另外,你代码里用memset清空整个 buckets 也是不必要的开销——因为每次循环都会覆盖 buckets 的内容,之前的旧值根本不会被读取,完全可以去掉这两行memset,这也能节省不少时间。

2. 如何进一步优化速度?MSB优先是否更好?

你的LSB实现已经很快了,但还有不少可以优化的点:

减少内存操作开销

  • 去掉不必要的memset:如刚才所说,每次循环都会重新填充 buckets,清空操作完全多余,直接删除memset语句能节省大量时间。
  • 避免memcpy的额外开销:当前每次循环都把 buckets 复制回原数组,其实可以用双缓冲的方式,直接交替使用原数组和 buckets 作为输入输出,减少一次复制。比如第一次把原数组分到 bucketsA 和 bucketsB,第二次直接用 bucketsA/B 作为输入,写到原数组或另一个 bucket,这样能避免频繁的memcpy。
  • 预计算桶的大小:当前是遍历数组时直接填充 buckets,你可以先统计当前位为0和1的元素数量,然后直接计算每个桶的起始位置,这样可以避免多次移动元素,直接一次性把元素放到正确的位置(这是基数排序的标准优化,叫"计数阶段")。比如:
    // 先统计计数
    int count0 = 0, count1 = 0;
    for (int j = 0; j < length; j++) {
        unsigned int conversion = *(unsigned int *)&array[j];
        if (((conversion >> i) & 1) == 0) count0++;
        else count1++;
    }
    // 计算位置
    int pos0 = 0;
    int pos1 = count0;
    // 再填充数组
    for (int j = 0; j < length; j++) {
        unsigned int conversion = *(unsigned int *)&array[j];
        if (((conversion >> i) & 1) == 0) {
            buckets[pos0++] = array[j];
        } else {
            buckets[pos1++] = array[j];
        }
    }
    
    这样填充的顺序更规整,缓存命中率更高。

利用CPU缓存优化

  • 更大的基数粒度:当前每次按1位来排序(bits参数应该是32?),可以考虑按4位或8位一组来排序(即基数选16或256),这样循环次数从32次减少到8次或4次,虽然每次循环的工作量增加,但整体的循环开销和内存操作次数会减少,缓存利用率也更高。不过要注意浮点数符号位的处理,需要调整分组的顺序。
  • 预先转换类型:你每次循环都把float转成unsigned int,可以考虑预先把整个数组转换成unsigned int数组,只做一次转换,而不是每次循环都转换,这样能节省大量的类型转换操作。比如:
    // 预先转换整个数组
    unsigned int *uint_array = malloc(length * sizeof(unsigned int));
    for (int j = 0; j < length; j++) {
        uint_array[j] = *(unsigned int *)&array[j];
    }
    // 后续排序用uint_array来处理位运算
    // 最后再转换回float(如果需要的话)
    

MSB优先的优劣

MSB优先的基数排序(通常叫"最高位优先")在某些场景下有优势,比如可以提前终止排序(当某个桶里的元素已经有序时),但对于大规模的数组来说,LSB优先通常更高效——因为LSB的实现更简单,内存访问更连续,缓存命中率更高。MSB需要递归或者更复杂的桶管理,反而会增加开销,所以你的LSB方向是对的,不用轻易切换到MSB。

其他小优化

  • inline辅助函数:把nthBit声明为inline函数,让编译器把它展开,避免函数调用的开销。
  • 编译器优化选项:开启最高级别的编译器优化,比如GCC的-O3 -march=native,让编译器自动做循环展开、指令重排等优化,这能带来非常明显的速度提升。
  • 分离冗余计算:你的代码里在第一次循环时计算sum,这部分可以移到排序外面,不要和排序逻辑混在一起,减少循环内的工作量。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 13:53:16