数据管道:C 排序与 Python 排序性能对比实验 (C vs Python Pipeline)


章节概述

“Python 太慢了,C 才是性能之王”——这个说法对不对?本章设计一个完整的对比实验来定量回答。用 Python 编排整个实验流程(生成数据→启动 C 程序→读取结果→计时比较),用 C 实现经典的 qsort 排序算法。实验涉及 subprocess 进程管道、struct 模块二进制 I/O、time.perf_counter 高精度计时,展示”C 做计算,Python 做编排”的经典分工模式。

核心理念:Python 的”慢”在不同场景下差异巨大。如果用 Python 调用 NumPy(底层 C 实现)做矩阵运算,它和纯 C 一样快;如果用 Python 的 sorted()(C 实现的 Timsort)排序,它甚至可能比你自己写的 C 排序更快。本章的对比实验不是为了证明谁”更好”,而是让你理解性能差异的根源——算法、底层实现和调用开销。


第一节:实验设计与架构


实验流程如下:

graph TB
 subgraph Pipeline["Python 编排脚本"]
 S1["1. 生成 N 个随机整数"] --> S1A["写入 data.bin (二进制)"]
 S1A --> S2["2. 启动 C 排序进程"]
 S2 --> S2A["./c_sorter data.bin out.bin"]
 S1A --> S3["3. Python sorted()"]
 S3 --> S3A["原地排序"]
 S2A --> S4["4. 验证两个输出结果一致性"]
 S3A --> S4
 S4 --> S5["5. 高精度计时对比<br/>(time.perf_counter)"]
 S5 --> S6["6. 输出性能报告"]
 end

为什么用二进制格式而非文本?

  • 文本读写的解析开销(atoi / str() 转换)在百万级数据规模下成为瓶颈
  • 二进制 int32_t 数组直接映射到内存,C 侧 fread / Python 侧 struct.unpack 零解析开销
  • 模拟实际场景:C 程序通常操作原始二进制数据(网络包、文件格式、硬件寄存器)

实验可配置参数(JSON)

{
 "data_sizes": [1000, 10000, 100000, 1000000],
 "trials": 5,
 "output_csv": "benchmark_results.csv"
}

对每个数据规模运行多次取中位数,减小系统波动的影响。


第二节:C 侧——qsort 排序程序


c_sorter.c —— 读取二进制 int32 数组,排序,写回:

// c_sorter.c —— 读取二进制 int32 数组,用 qsort 排序,写回
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
 
int cmp_int32(const void *a, const void *b) {
 int32_t ia = *(const int32_t *)a;
 int32_t ib = *(const int32_t *)b;
 return (ia > ib) - (ia < ib); // 安全的三路比较
}
 
int main(int argc, char *argv[]) {
 if (argc != 3) {
 fprintf(stderr, "用法: %s <输入文件> <输出文件>\n", argv[0]);
 return 1;
 }
 
 FILE *fin = fopen(argv[1], "rb");
 if (!fin) { perror("fopen input"); return 2; }
 
 // 获取文件大小,推算元素个数
 fseek(fin, 0, SEEK_END);
 long file_size = ftell(fin);
 rewind(fin);
 
 size_t count = file_size / sizeof(int32_t);
 if (count == 0) {
 fprintf(stderr, "文件为空或大小不对齐\n");
 fclose(fin);
 return 3;
 }
 
 int32_t *data = (int32_t *)malloc(file_size);
 if (!data) { perror("malloc"); fclose(fin); return 4; }
 
 size_t read_count = fread(data, sizeof(int32_t), count, fin);
 fclose(fin);
 
 if (read_count != count) {
 fprintf(stderr, "读取不完整: 期望 %zu, 实际 %zu\n", count, read_count);
 free(data);
 return 5;
 }
 
 // ---- 核心:调用 C 标准库 qsort ----
 qsort(data, count, sizeof(int32_t), cmp_int32);
 
 FILE *fout = fopen(argv[2], "wb");
 if (!fout) { perror("fopen output"); free(data); return 6; }
 
 fwrite(data, sizeof(int32_t), count, fout);
 fclose(fout);
 free(data);
 
 printf("C qsort: 已排序 %zu 个整数 → %s\n", count, argv[2]);
 return 0;
}

编译:

gcc -Wall -Wextra -O2 -std=c11 -o c_sorter c_sorter.c

注意使用 -O2 编译——公平对比应在双方都优化的条件下进行。C 的 -O0 调试模式会显著慢于实际生产环境,不应作为性能基准。

为什么用 qsort 而非手写排序?

  • qsort 是 C 标准库的通用排序函数,经过数十年优化,是合理的 C 侧性能代表
  • 手写排序取决于你的实现水平,引入不公平变量
  • 实验中”Python 的 Timsort vs C 标准库 qsort”是客观的库对库比较

第三节:Python 编排脚本——完整实验


#!/usr/bin/env python3
"""C vs Python 排序性能对比实验"""
import subprocess
import struct
import random
import time
import os
import sys
 
def generate_data(count, seed=42):
 """生成 count 个随机 int32 整数(小端序二进制)"""
 random.seed(seed)
 data = [random.randint(-2**30, 2**30 - 1) for _ in range(count)]
 packed = struct.pack(f'<{count}i', *data)
 return data, packed
 
def run_c_sort(input_path, output_path):
 """运行 C 排序程序,返回耗时(秒)"""
 t0 = time.perf_counter()
 result = subprocess.run(
 ['./c_sorter', input_path, output_path],
 capture_output=True,
 text=True,
 timeout=120
 )
 elapsed = time.perf_counter() - t0
 if result.returncode != 0:
 raise RuntimeError(f"C 排序失败: {result.stderr}")
 return elapsed
 
def read_binary_output(output_path, count):
 """读取 C 排序后的二进制结果"""
 with open(output_path, 'rb') as f:
 raw = f.read()
 return list(struct.unpack(f'<{count}i', raw))
 
def run_python_sort(data):
 """运行 Python sorted(),返回 (耗时, 排序结果)"""
 t0 = time.perf_counter()
 sorted_data = sorted(data)
 elapsed = time.perf_counter() - t0
 return elapsed, sorted_data
 
def run_benchmark(sizes, trials=5, seed=42):
 results = []
 
 for size in sizes:
 print(f"\n{'='*60}")
 print(f"数据规模: {size:,} 个 int32 ({size*4/1024/1024:.2f} MB)")
 print(f"测试轮次: {trials}")
 print(f"{'='*60}")
 
 c_times = []
 py_times = []
 
 for trial in range(trials):
 trial_seed = seed + trial
 
 # 生成数据(每次用不同 seed 避免缓存效应)
 data, packed = generate_data(size, trial_seed)
 
 input_file = f'/tmp/bench_{size}_{trial}.bin'
 output_file = f'/tmp/bench_{size}_{trial}_out.bin'
 
 with open(input_file, 'wb') as f:
 f.write(packed)
 
 # ---- C 排序 ----
 c_elapsed = run_c_sort(input_file, output_file)
 c_times.append(c_elapsed)
 
 # 验证 C 排序结果
 c_result = read_binary_output(output_file, size)
 assert c_result == sorted(data), f"C 排序结果错误!(trial {trial})"
 
 # ---- Python 排序 ----
 py_elapsed, py_result = run_python_sort(data)
 py_times.append(py_elapsed)
 
 assert py_result == c_result, f"C 与 Python 排序结果不一致!(trial {trial})"
 
 # 清理临时文件
 os.remove(input_file)
 os.remove(output_file)
 
 print(f" 第 {trial+1}/{trials} 轮: C={c_elapsed*1000:.1f}ms Python={py_elapsed*1000:.1f}ms")
 
 c_median = sorted(c_times)[len(c_times)//2]
 py_median = sorted(py_times)[len(py_times)//2]
 ratio = c_median / py_median if py_median > 0 else 0
 
 results.append({
 'size': size,
 'c_median_ms': round(c_median * 1000, 3),
 'py_median_ms': round(py_median * 1000, 3),
 'ratio': round(ratio, 3),
 'faster': 'C' if c_median < py_median else 'Python',
 })
 
 print(f"\n 中位数: C={c_median*1000:.1f}ms Python={py_median*1000:.1f}ms → {results[-1]['faster']}{max(ratio, 1/ratio):.1f}x")
 
 return results
 
def print_report(results):
 print("\n" + "=" * 70)
 print(" 最终性能对比报告".center(60))
 print("=" * 70)
 print(f"{'规模':>12} | {'C (ms)':>10} | {'Python (ms)':>12} | {'快慢比':>8} | {'胜出':>8}")
 print("-" * 70)
 for r in results:
 print(f"{r['size']:>12,} | {r['c_median_ms']:>10.1f} | {r['py_median_ms']:>12.1f} | {r['ratio']:>8.2f} | {r['faster']:>8}")
 print("-" * 70)
 
if __name__ == '__main__':
 import argparse
 parser = argparse.ArgumentParser(description='C vs Python 排序性能对比')
 parser.add_argument('--sizes', nargs='+', type=int, default=[1000, 10000, 100000, 1000000],
 help='测试数据规模列表')
 parser.add_argument('--trials', type=int, default=5, help='每个规模的测试轮次')
 parser.add_argument('--seed', type=int, default=42, help='随机种子')
 args = parser.parse_args()
 
 if not os.path.exists('./c_sorter'):
 print("请先编译 C 排序程序: gcc -O2 -o c_sorter c_sorter.c", file=sys.stderr)
 sys.exit(1)
 
 results = run_benchmark(args.sizes, args.trials, args.seed)
 print_report(results)

运行

# 先编译 C 程序
gcc -Wall -Wextra -O2 -std=c11 -o c_sorter c_sorter.c
 
# 运行对比实验
python benchmark.py --sizes 1000 10000 100000 1000000 --trials 5

典型输出(Intel i7, Linux):

======================================================================
数据规模: 1,000,000 个 int32 (3.81 MB)
测试轮次: 5
======================================================================
 第 1/5 轮: C=189.2ms Python=201.5ms
 第 2/5 轮: C=187.8ms Python=203.1ms
 第 3/5 轮: C=188.5ms Python=200.8ms
 第 4/5 轮: C=186.9ms Python=202.4ms
 第 5/5 轮: C=189.0ms Python=199.7ms

 中位数: C=188.5ms Python=201.5ms → C 快 1.1x

======================================================================
 最终性能对比报告
======================================================================
 规模 | C (ms) | Python (ms) | 快慢比 | 胜出
----------------------------------------------------------------------
 1,000 | 0.2 | 0.1 | 1.59 | Python
 10,000 | 1.8 | 1.4 | 1.35 | Python
 100,000 | 19.2 | 18.9 | 1.01 | Python
 1,000,000 | 188.5 | 201.5 | 0.94 | C
----------------------------------------------------------------------

出乎意料?小数据量下 Python 的 Timsort 甚至比 C 的 qsort 更快!原因:Python 的 sorted() 底层是 C 语言实现的 Timsort 算法,在现代 CPU 上对小数组做了高度优化(利用已排序区间);而你的实验程序额外包含了 I/O 开销(文件读写)。


第四节:深入分析——排除 I/O 开销


上一节的实验把 I/O 时间计入了 C 排序耗时。如果想纯比较排序算法本身的性能,可以修改架构:让 C 程序内置数据生成,Python 只计时进程运行。

改进版 C 程序 c_sorter_v2.c(无 I/O 开销版):

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
 
int cmp_int32(const void *a, const void *b) {
 int32_t ia = *(const int32_t *)a;
 int32_t ib = *(const int32_t *)b;
 return (ia > ib) - (ia < ib);
}
 
int main(int argc, char *argv[]) {
 if (argc != 2) {
 fprintf(stderr, "用法: %s <元素个数>\n", argv[0]);
 return 1;
 }
 
 size_t count = strtoull(argv[1], NULL, 10);
 int32_t *data = (int32_t *)malloc(count * sizeof(int32_t));
 if (!data) { perror("malloc"); return 2; }
 
 // 生成随机数据(使用固定种子保证可复现)
 srand(42);
 for (size_t i = 0; i < count; i++)
 data[i] = (int32_t)((rand() << 16) | rand());
 
 // 排序(这里开始计时由外部 Python 完成)
 qsort(data, count, sizeof(int32_t), cmp_int32);
 
 free(data);
 printf("Done\n");
 return 0;
}

对应的 Python 测试代码:

def run_c_sort_pure(count):
 """计时纯排序(不含 I/O)"""
 t0 = time.perf_counter()
 result = subprocess.run(
 ['./c_sorter_v2', str(count)],
 capture_output=True,
 text=True,
 timeout=60
 )
 elapsed = time.perf_counter() - t0
 assert result.returncode == 0
 return elapsed

这种方式的计时误差是进程启动开销(fork + exec),但 C 和 Python 排序的启动开销在同数量级,不影响相对比较。

公平对比的纯 Python 侧

def run_python_sort_pure(count):
 """计时纯 Python sorted()(不含数据生成)"""
 import random, time
 random.seed(42)
 data = [(random.randint(0, 2**31) << 16) | random.randint(0, 2**16) for _ in range(count)]
 
 t0 = time.perf_counter()
 result = sorted(data)
 elapsed = time.perf_counter() - t0
 return elapsed

第五节:数据可视化与结论


将实验结果导出为 CSV 后,可以用 Python 生态做可视化(详见 数据可视化教程)。这里给出快速制图代码:

import csv
 
# 保存实验结果
with open('benchmark_results.csv', 'w', newline='') as f:
 writer = csv.DictWriter(f, fieldnames=['size','c_median_ms','py_median_ms','ratio','faster'])
 writer.writeheader()
 writer.writerows(results)
 
# 快速文本图表(无需 matplotlib)
print("\n性能对比 (中位数, ms):")
print(f"{'规模':>12} | {'C':>10} | {'Python':>10} | {'差异'}")
print("-" * 50)
for r in results:
 bar_c = '█' * int(r['c_median_ms'] / max(r['c_median_ms'] for r in results) * 20)
 bar_py = '█' * int(r['py_median_ms'] / max(r['py_median_ms'] for r in results) * 20)
 print(f"{r['size']:>12,} | {r['c_median_ms']:>8.1f}ms {bar_c}")
 print(f"{'':>12} | {r['py_median_ms']:>8.1f}ms {bar_py}")
 print()

实验结论总结

维度结论
排序算法本身Python Timsort (C 实现) ≈ C qsort,差距在常数因子内
I/O 开销C 读取文件 + Python 写入文件的开销可能在排序时间中占主导
开发效率Python 实验脚本 ~100 行;如果全用 C 写,代码量 3-5 倍
最佳实践Python 编排实验、管理临时文件、生成报告;C 负责核心计算
”Python 很慢”?取决于你在做什么:手写循环确实慢,但调用底层 C 实现的库时几乎等价于 C

最重要的收获:学会这个实验模式后,你可以用它测试自己的 C 算法——用 Python 做测试 harness,用 C 做运算核心,两者通过进程管道或二进制文件交换数据。这是本教程最核心的”C ↔ Python 协作”范式。


练习

以下题目用于验证本章所学内容:

题号题目链接涉及知识点
912排序数组https://leetcode.cn/problems/sort-an-array/排序算法、性能对比
88合并两个有序数组https://leetcode.cn/problems/merge-sorted-array/数组合并、排序
215数组中的第K个最大元素https://leetcode.cn/problems/kth-largest-element-in-an-array/排序、堆