Mac与Windows下预分配vs重新分配性能差异原因探究
内存分配方式的跨平台性能差异探究
问题背景
探究两种内存分配策略的耗时差异:
- 预分配结构后,每次循环仅重置填充默认值
- 每次循环重新分配完整结构
在Windows和WLS Ubuntu环境中,重新分配的耗时远高于预分配,但Mac平台上两者性能差距极小。疑问:Mac的编译器是否将重新分配优化为仅分配一次再填充?为何出现这种差异?
测试代码
#include <algorithm> #include <random> #include <thread> #include <iostream> #include <chrono> #include <vector> #include <numeric> using namespace std; template<typename T> inline double getMs(T start, T end) { return double( std::chrono::duration_cast<std::chrono::milliseconds>(end - start) .count()) / 1000; } void pre_allocation(int size, int max_k) { vector<int> tau_star(size, 1440 * 3); tau_star.shrink_to_fit(); vector<vector<int>> tau; tau.reserve(max_k); vector<vector<int>> pred_trip; pred_trip.reserve(max_k); vector<vector<int>> pred_stop; pred_stop.reserve(max_k); for (int k = 0; k < max_k; ++k) { pred_trip.emplace_back(size, -1); pred_stop.emplace_back(size, std::numeric_limits<size_t>::max()); tau.emplace_back(size, 1440 * 3); } for (size_t i = 0; i < size; i++) { std::fill(tau_star.begin(), tau_star.end(), 1440 * 3); for (int k = 0; k < max_k; ++k) { std::fill(tau[k].begin(), tau[k].end(), 1440 * 3); std::fill(pred_trip[k].begin(), pred_trip[k].end(), -1); std::fill(pred_stop[k].begin(), pred_stop[k].end(), std::numeric_limits<size_t>::max()); } } } void re_allocation(int size, int max_k) { for (size_t i = 0; i < size; i++) { vector<int> tau_star(size, 1440 * 3); tau_star.shrink_to_fit(); vector<vector<int>> tau; tau.reserve(max_k); vector<vector<int>> pred_trip; pred_trip.reserve(max_k); vector<vector<int>> pred_stop; pred_stop.reserve(max_k); for (int k = 0; k < max_k; ++k) { pred_trip.emplace_back(size, -1); pred_stop.emplace_back(size, std::numeric_limits<size_t>::max()); tau.emplace_back(size, 1440 * 3); } } } int main(int) { int size = 107333; int max_k = 3; auto start_pre_alloc = std::chrono::high_resolution_clock::now(); pre_allocation(size, max_k); double elapsed_pre_alloc = getMs(start_pre_alloc, std::chrono::high_resolution_clock::now()); auto start_re_alloc = std::chrono::high_resolution_clock::now(); re_allocation(size, max_k); double elapsed_re_alloc = getMs(start_re_alloc, std::chrono::high_resolution_clock::now()); printf("Time in pre-allocation: %.3f sec\n", elapsed_pre_alloc); printf("Time in RE-allocation: %.3f sec\n", elapsed_re_alloc); return 0; }
测试结果
Windows
Time in pre-allocation: 11.617 sec Time in RE-allocation: 53.679 sec
WLS Ubuntu
Time in pre-allocation: 15.749 sec Time in RE-allocation: 81.905 sec
Mac
Time in pre-allocation: 9.396 sec Time in RE-allocation: 12.408 sec
硬件与编译环境
Windows
- CPU:11th Gen Intel(R) Core(TM) i7-11700KF @ 3.60GHz
- RAM:16 GB DDR4
- 编译环境:MS Visual Studio 2022
Mac
- CPU:6-core Intel Core i9 @2.9 GHz
- RAM:16GB 2400 MHz DDR4
- 系统:macOS Big Sur Version 11.6.5
- 编译环境:Apple Clang(通过
g++命令调用),编译参数-O2 -ffast-math -DNDEBUG -Wall -std=c++17
WLS Ubuntu
- 编译环境:GCC,编译参数
-O2 -ffast-math -march=native -DNDEBUG -Wall -std=c++17 -DLINUX
编译使用的Makefile:
# Directory for my files MYHOME = ${PWD} BIN = ${MYHOME}/bin LIB = ${MYHOME}/lib SRC = ${MYHOME} # For Linux: # OPTFLAG = -O2 -ffast-math -march=native -DNDEBUG -Wall -std=c++17 -DLINUX -Wall # For Mac: OPTFLAG = -O2 -ffast-math -DNDEBUG -Wall -std=c++17 -DLINUX -Wall LDFLAGS = -O2 -DNDEBUG -lm -pthread -std=c++17 COMPILER = g++ ${OPTFLAG} LINKER = g++ ${LDFLAGS} # Directory for output files OUT_DIR=bin lib # Command line tool cli: ${OUT_DIR} ${SRC}/main.cpp ${COMPILER} -c ${SRC}/main.cpp -o ${LIB}/main.o ${LINKER} -o ${BIN}/main ${LIB}/main.o
原因分析
编译器优化策略差异
Mac上的g++实际是Apple Clang的前端,在-O2优化级别下,它对循环内的重复内存分配做了激进优化:识别到每次循环创建的容器大小固定、生命周期严格限制在单次迭代内,因此会复用同一块内存区域(栈内存或堆内存缓存),避免每次都调用malloc/free这类开销较大的系统调用。内存分配的隐式优化
代码中re_allocation函数的容器仅做初始化填充,没有后续的读写操作(除了初始化),Clang的优化器可能进一步将堆分配转为栈分配(只要内存大小在栈允许范围内),或者利用操作系统的内存页缓存,大幅减少实际的内存申请/释放开销。其他平台的保守优化
Windows的MSVC和WLS的GCC在处理这类场景时优化策略相对保守,更严格遵循代码的字面逻辑,每次循环都会执行完整的内存申请、初始化、释放流程,导致耗时显著高于预分配策略。
若要验证这一点,可以尝试:
- 在
re_allocation中对容器元素做实际读写操作(比如修改后输出),观察优化是否被抑制 - 添加编译参数
-fno-elide-constructors关闭构造函数省略,或-O0关闭优化,看耗时是否上升
内容的提问来源于stack exchange,提问作者Claudio Tomasi
相关产品推荐
相关产品推荐

