Repository navigation
Conversation
``` Products/bench_alignment_vectorization --benchmark_repetitions=5 --benchmark_report_aggregates_only=true --benchmark_out=../results/alignment_default.json --benchmark_out_format=json Products/bench_alignment_vectorization_native --benchmark_repetitions=5 --benchmark_report_aggregates_only=true --benchmark_out=../results/alignment_native.json --benchmark_out_format=json ``` Results: on macOS M1 Max Median of 5, ns. "default" = -O3 -ffp-contract=off (like EnergyPlus), "native" = -O3 -mcpu=native -ffast-math -fno-finite-math-only (like NatLabRockies/EnergyPlus#11816). Sizes 1 Ki / 32 Ki / 4 Mi doubles. | Kernel | default, 64-aligned | native, 64-aligned | native speedup | native, misaligned +8 B vs 64-aligned | |---------|-------------------------------|-----------------------------|--------------------|---------------------------------------| | Sum | 849 / 31,338 / 4,038,135 | 99 / 3,947 / 544,375 | 8.6x / 7.9x / 7.4x | +0 / +1 / +1% | | Dot | 874 / 31,398 / 4,036,528 | 138 / 6,350 / 1,072,154 | 6.3x / 4.9x / 3.8x | +2 / +3 / 0% | | Cubic | 255 / 7,850 / 1,090,068 | 182 / 5,687 / 1,086,287 | 1.4x / 1.4x / 1.0x | +3 / +7 / 0% | | Axpy | 132 / 7,529 / 1,071,804 | 131 / 7,548 / 1,074,188 | 1.0x | +32 / +14 / +2% | | Stencil | 333 / 9,142 / 2,105,869 | 325 / 9,109 / 2,056,428 | 1.0x | 0 / +1 / -24% (artifact, see below) | Findings: - Alignment: Default, Aligned64 and Aligned128 are within noise everywhere, in both builds, and std::assume_aligned<64> adds nothing. Only misalignment within the vector width costs anything, and only for streaming kernels on cache-resident data (Axpy +32% in L1, +14% in L2 in the native build, +38% / +13% in the default build). - Native + fast math: reductions vectorize thanks to reassociation (Sum 7-9x, Dot 4-6x, Dot is bandwidth bound at 4 Mi), polynomial evaluation gains 1.4x in cache from FMA contraction (disabled by -ffp-contract=off in the default build). Axpy and Stencil are unchanged: already vectorized in the default build, or memory bound. Caveats (to be addressed in a follow-up): - macOS libmalloc gave std::vector 128-byte (or better) alignment here: allocations >= 32 KiB are page-aligned (16 KiB pages), and the 8 KiB one was 128-aligned by luck (small allocations are only guaranteed 16 bytes, measured). So "Default vs Aligned64" did not test the 16-byte case that glibc/MSVC give: needs explicit Offset16 / Offset32 layouts. - Power-of-two sizes: no vector-loop tails, and cache set aliasing between in/out buffers (most likely the source of the Stencil -24% at 4 Mi, present in both builds). Real EnergyPlus array sizes are small and not powers of two (37, 251, 1003...).
…counters, and a script to compare results Offset8/16/32 view a 128-byte aligned buffer from +8/+16/+32 bytes so the alignment does not depend on the allocator (Offset16 is what std::vector<double> gets on glibc/MSVC). Sizes are 37, 251, 1003, 30011, 4000037 doubles: powers of two hid vector-loop tails and caused cache set aliasing (the Stencil -27% at 4 Mi). compare_alignment_results.py prints Markdown tables relative to Aligned64 from one or two (default, native) result files. ``` Products/bench_alignment_vectorization --benchmark_repetitions=5 --benchmark_report_aggregates_only=true --benchmark_out=../results/alignment_default.json --benchmark_out_format=json Products/bench_alignment_vectorization_native --benchmark_repetitions=5 --benchmark_report_aggregates_only=true --benchmark_out=../results/alignment_native.json --benchmark_out_format=json python3 compare_alignment_results.py results/alignment_default.json results/alignment_native.json --offsets ``` Results on two machines: macOS M1 Max (Apple clang, NEON 16-byte vectors), and Linux Ryzen 7 1800X (Zen 1, GCC, AVX2 32-byte vectors). Median of 5, max coefficient of variation 8.7% (M1) / 5.2% (Ryzen). What the allocator actually gives std::vector<double> (Default layout, observed): glibc (Ryzen) 16-byte aligned at all sizes (offsets 0/16/32/48/80/96/112 mod 128). macOS 16-byte guaranteed for small blocks, page-aligned for large ones (>= ~32 KiB, 16 KiB pages). __STDCPP_DEFAULT_NEW_ALIGNMENT__ is 16 on Apple clang, GCC and MSVC. Alignment, % vs Aligned64 (ie OBJEXXFCL_ALIGN=64): | Layout | M1 Max | Ryzen 1800X | |----------------------------------------|-----------------------------------------|-------------------------------------------| | Default / Aligned128 | within noise | within noise | | Offset16 (glibc/MSVC std::vector) | within noise, both builds | within noise, both builds (*) | | Offset32 (AVX2-aligned, not 64) | within noise | within noise (*) | | Offset8, Axpy, default / native build | +12 to +16% / +11 to +22% (n 251-30011) | +17 to +28% / +14 to +34% (n 37-30011) | | Offset8, Cubic | up to +8% | +6 to +10% (native) | (*) Two tiny-size anomalies on the Ryzen that are not alignment driven: Cubic native n=37 is +60/+70% for Offset16 and Default but also +58% for Aligned128 and +1% for Offset8; Stencil default n=37 Offset32 +17%. Most likely relative placement of input/output buffers (store/load address aliasing), not 16 vs 64. Native (-march/-mcpu=native -ffast-math -fno-finite-math-only) speedup vs default build (-O3 -ffp-contract=off), Aligned64: | Kernel | M1 Max | Ryzen 1800X | |-------------------|---------------------------------|----------------------------------| | Sum (n >= 251) | 7.2-8.5x (1.1x at n=37) | 2.0x (1.8x at 4M, 1.5x at n=37) | | Dot (n >= 251) | 4.5-6.3x (1.7x at n=37) | 2.0x (1.5x at 4M, 1.9x at n=37) | | Cubic (in cache) | 1.26-1.38x | 1.41-1.48x | | Axpy | 1.0-1.07x | 1.0-1.08x | | Stencil | 1.0x | 0.89-0.97x (slower at mid sizes) | std::assume_aligned<64>: no effect on the M1. On the Ryzen it helps Sum in the native build (-27% at n=37, -11% at 251, -8% at 4M: GCC drops the alignment prologue), but the hinted Axpy is 30-35% slower in the default build (n 37-1003). EnergyPlus never used such hints (ASSUME_ALIGNED_OBJEXXFCL is unused), so OBJEXXFCL_ALIGN=64 never got that benefit. Conclusions: - Going from ObjexxFCL's 64-byte alignment to std::vector's 16 bytes costs nothing measurable on either machine, in default or native builds. Alignment is not a reason to keep ObjexxFCL arrays. - Only misalignment within an element (Offset8) costs, more so on Zen 1 AVX2, but no allocator produces that for double. - Native + fast math gains depend heavily on the CPU (reductions 7-8x on M1, 2x on Zen 1 which splits 256-bit ops in two) and on size (little at tens of elements, which is typical of EnergyPlus arrays). Supports keeping NatLabRockies/EnergyPlus#11816 opt-in. Excluded outliers (single block, not reproduced in the other build): M1 default Stencil Aligned128 4M +146% (buffer placement, Aligned64/Default have the same alignment at that size), M1 default Axpy Aligned64 4M reference 15% slow (cv 5.5%; AxpyAssumeAligned64 on the same layout was normal).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Does array alignment matter, with and without native optimizations + fast math?
Context: EnergyPlus builds ObjexxFCL arrays with
OBJEXXFCL_ALIGN=64(first element aligned to 64 bytes), a setting from ObjexxFCL 4.1 (NatLabRockies/EnergyPlus#5395) meant to help vectorization, at a time when-Ofastwas also proposed (and rejected). Replacing ObjexxFCL arrays bystd::vector/std::arraydrops that to the default allocator alignment. And NatLabRockies/EnergyPlus#11816 adds an opt-in-march/-mcpu=native+-ffast-mathbuild, which is when vectorization (and so maybe alignment) can actually kick in.What's in here
bench_alignment_vectorization.cpp: five EnergyPlus-like kernels (Sum,Dot,Axpy,Cubiccurve evaluation, 5-pointStencilon a row-major grid) taking astd::span(so the compiler cannot know the alignment, like EnergyPlus going throughArray::operator()), plusstd::assume_aligned<64>variants ofSumandAxpy.Default(std::vector<double>),Aligned64(likeOBJEXXFCL_ALIGN=64),Aligned128(Apple Silicon cache line), andOffset8/Offset16/Offset32(a 128-byte aligned buffer viewed from +8/+16/+32 bytes, so the alignment does not depend on the allocator;Offset16is whatstd::vector<double>gets on glibc/MSVC).*_offset_mod_128counters report the alignment each buffer actually got.CMakeLists.txt: the file is built twice:bench_alignment_vectorization: EnergyPlus-like default flags-O3 -ffp-contract=offbench_alignment_vectorization_native:-O3 -march=native(-mcpu=nativeon ARM)-ffast-math -fno-finite-math-only, like Fix #5451 - Allow enabling compiler native optimizations and fast math NatLabRockies/EnergyPlus#11816compare_alignment_results.py: Markdown tables relative toAligned64from one or two (default, native) result files, with--offsetsto list observed buffer alignments.A full run takes ~10 min per executable;
--benchmark_repetitions=3 --benchmark_min_time=0.1sis enough for a quick look.Results
Two machines: macOS M1 Max (Apple clang, NEON 16-byte vectors) and Linux Ryzen 7 1800X (Zen 1, GCC, AVX2 32-byte vectors). Median of 5, max coefficient of variation 8.7% (M1) / 5.2% (Ryzen).
What the allocator actually gives
std::vector<double>__STDCPP_DEFAULT_NEW_ALIGNMENT__is 16 on Apple clang, GCC and MSVC: that's allstd::allocator<double>guarantees.Defaultis often "luckily" as aligned asAligned128, hence the explicitOffsetNlayouts.Alignment, % vs
Aligned64(ieOBJEXXFCL_ALIGN=64)Default/Aligned128Offset16(glibc/MSVCstd::vector)Offset32(AVX2-aligned, not 64)Offset8,Axpy, default / native buildOffset8,Cubic(*) Two tiny-size anomalies on the Ryzen that are not alignment driven:
Cubicnative n=37 is +60/+70% forOffset16andDefault, but also +58% forAligned128and only +1% forOffset8;Stencildefault n=37Offset32+17%. Most likely relative placement of input/output buffers (store/load address aliasing), not 16 vs 64.Native + fast math speedup vs default build (
Aligned64)Sum(n >= 251)Dot(n >= 251)Cubic(in cache)AxpyStencilstd::assume_aligned<64>No effect on the M1. On the Ryzen it helps
Sumin the native build (-27% at n=37, -11% at 251, -8% at 4M: GCC drops the alignment prologue), but the hintedAxpyis 30-35% slower in the default build (n 37-1003). EnergyPlus never used such hints (ASSUME_ALIGNED_OBJEXXFCLis unused), soOBJEXXFCL_ALIGN=64never got that benefit.Conclusions
std::vector's 16 bytes costs nothing measurable on either machine, in default or native builds. Alignment is not a reason to keep ObjexxFCL arrays.Offset8) costs, more so on Zen 1 AVX2, but no allocator produces that fordouble.Excluded outliers, and the first (power-of-two) run
StencilAligned1284M +146% (buffer placement:Aligned64/Defaulthave the same alignment at that size); M1 defaultAxpyAligned644M reference 15% slow (cv 5.5%;AxpyAssumeAligned64on the same layout was normal).Misaligned8layout. Same conclusions on the M1, butDefaultwas page-aligned or lucky on macOS (so it never tested the 16-byte case), andStencilshowed a -27% "misaligned is faster" artifact at 4 Mi from cache set aliasing between the power-of-two buffers, which disappears with non-power-of-two sizes.🤖 Generated with Claude Code