Core: Use a scalar tail for inner-unrolled partial-packet assignments

libeigen/eigen!2866

Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
This commit is contained in:
Rasmus Munk Larsen
2026-08-19 08:17:54 -07:00
co-authored by Rasmus Munk Larsen
parent 7c8275ef88
commit 303c4c54db
2 changed files with 97 additions and 4 deletions
+12 -3
View File
@@ -132,12 +132,14 @@ struct copy_using_evaluator_traits {
static constexpr int ActualPacketSize = Vectorized ? unpacket_traits<PacketType>::size : 1;
static constexpr int UnrollingLimit = EIGEN_UNROLLING_LIMIT * ActualPacketSize;
static constexpr int CoeffReadCost = int(DstEvaluator::CoeffReadCost) + int(SrcEvaluator::CoeffReadCost);
static constexpr bool MayUnrollCompletely =
(SizeAtCompileTime != Dynamic) && (SizeAtCompileTime * CoeffReadCost <= UnrollingLimit);
static constexpr bool MayUnrollInner =
(InnerSizeAtCompileTime != Dynamic) && (InnerSizeAtCompileTime * CoeffReadCost <= UnrollingLimit);
public:
// True when the whole assignment is a fixed-size kernel cheap enough to emit as straight-line
// code. Selects CompleteUnrolling, and gates the scalar tail in the SliceVectorized loop below.
static constexpr bool MayUnrollCompletely =
(SizeAtCompileTime != Dynamic) && (SizeAtCompileTime * CoeffReadCost <= UnrollingLimit);
static constexpr int Unrolling =
(Traversal == InnerVectorizedTraversal || Traversal == DefaultTraversal)
? (MayUnrollCompletely ? CompleteUnrolling
@@ -704,7 +706,14 @@ struct dense_assignment_loop_impl<Kernel, SliceVectorizedTraversal, InnerUnrolli
static constexpr int PacketSize = unpacket_traits<PacketType>::size;
static constexpr int InnerSize = Kernel::AssignmentTraits::InnerSizeAtCompileTime;
static constexpr int VectorizableSize = numext::round_down(InnerSize, PacketSize);
static constexpr bool UsePacketSegment = Kernel::AssignmentTraits::UsePacketSegment;
// The tail length is a compile-time constant here, so it can be emitted as a masked packet
// segment or as scalars. Scalars win for the same fixed-size kernels LinearVectorizedTraversal
// already emits them for: their destination is a temporary the enclosing expression reloads by
// packet, and on AVX a masked store forwards poorly to those loads. Everything else keeps the
// segment, where one packet evaluation replaces up to PacketSize - 1 scalar ones.
static constexpr bool UsePacketSegment =
Kernel::AssignmentTraits::UsePacketSegment && !Kernel::AssignmentTraits::MayUnrollCompletely;
using packet_loop = copy_using_evaluator_innervec_InnerUnrolling<Kernel, 0, VectorizableSize, Unaligned, Unaligned>;
using packet_segment_loop = copy_using_evaluator_innervec_segment<Kernel, VectorizableSize, InnerSize, Unaligned,
+85 -1
View File
@@ -21,7 +21,15 @@
// ops where each result is consumed by the next. A partial-packet tail store
// then collides with the consumer's packet load -- the store-to-load
// forwarding hazard that makes a masked tail far more costly than a scalar
// one. This is the cost Parts A and B cannot see in isolation.
// one. This is the cost Parts A and B cannot see in isolation. Chained/Rotation
// additionally covers the lazy-product shape, whose tail is reached through
// SliceVectorizedTraversal with InnerUnrolling and through the product
// evaluator's own partial loads, not through LinearVectorizedTraversal.
//
// Part D takes the same traversal as Chained/Rotation but streams over a runtime
// outer size with nothing reloading the destination, so the segment amortizes a
// source evaluation instead of stalling a consumer. It is the shape that decides
// how narrowly the scalar tail may be applied.
//
// The active packet size and the value of has_packet_segment per scalar type
// are emitted as Google Benchmark custom context, so a captured run is
@@ -418,6 +426,66 @@ void BM_Chained_Block23(benchmark::State& state) {
}
}
// #3083 Example A: the matrix algebra of one IMU integration step. Its 3x3
// products are lazy, so unlike the kernels above the destination assignment
// takes SliceVectorizedTraversal with InnerUnrolling, and the product evaluator
// itself loads a partial packet. Neither path was covered by the kernels above,
// which is why this one kept a masked tail after !2581.
void BM_Chained_Rotation(benchmark::State& state) {
Matrix3d S = Matrix3d::Random(), R = Matrix3d::Random(), Xi = Matrix3d::Random();
Vector3d a = Vector3d::Random(), v = Vector3d::Zero();
const double dt = 0.005;
for (auto _ : state) {
benchmark::DoNotOptimize(S);
benchmark::DoNotOptimize(R);
benchmark::DoNotOptimize(a);
const Matrix3d S2 = S * S;
Xi = dt * Matrix3d::Identity() + 0.5 * dt * dt * S + (dt * dt * dt / 6.0) * S2;
v += R * Xi * a;
benchmark::DoNotOptimize(Xi);
benchmark::DoNotOptimize(v);
benchmark::ClobberMemory();
}
}
// ===========================================================================
// Part D : streaming slices (compile-time inner size, runtime outer size)
//
// SliceVectorizedTraversal with InnerUnrolling, as in Chained/Rotation, but the
// tail is paid once per column of a long runtime outer loop and no consumer
// reloads the destination. Sqrt makes one packet evaluation of the source far
// cheaper than PacketSize scalar ones, so unlike Part C this shape wants the
// masked segment; the copy variant is the cheap-source control.
// ===========================================================================
template <typename T, int Rows>
void BM_SliceCopy(benchmark::State& state) {
const Index cols = state.range(0);
using Mat = Matrix<T, Dynamic, Dynamic>;
Mat src = Mat::Random(Rows + 9, cols), dst = Mat::Zero(Rows + 9, cols);
for (auto _ : state) {
benchmark::DoNotOptimize(src.data());
dst.template topRows<Rows>() = src.template topRows<Rows>();
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * Rows * cols * static_cast<int64_t>(sizeof(T)) * 2);
}
template <typename T, int Rows>
void BM_SliceSqrt(benchmark::State& state) {
const Index cols = state.range(0);
using Mat = Matrix<T, Dynamic, Dynamic>;
Mat src = Mat::Random(Rows + 9, cols).cwiseAbs(), dst = Mat::Zero(Rows + 9, cols);
for (auto _ : state) {
benchmark::DoNotOptimize(src.data());
dst.template topRows<Rows>() = src.template topRows<Rows>().array().sqrt().matrix();
benchmark::DoNotOptimize(dst.data());
benchmark::ClobberMemory();
}
state.SetBytesProcessed(state.iterations() * Rows * cols * static_cast<int64_t>(sizeof(T)) * 2);
}
// ===========================================================================
// Registration
//
@@ -528,6 +596,18 @@ void add_all_prim_ct() {
add_prim_ct<T, 7>();
}
// Part D : one fixed inner size that is not a packet multiple for any supported
// packet width, swept over outer sizes that span the cache hierarchy.
template <typename T>
void add_slice() {
const std::string tag = std::string("/") + type_tag<T>() + "/7";
for (auto entry : {std::make_pair("Slice/copy" + tag, &BM_SliceCopy<T, 7>),
std::make_pair("Slice/sqrt" + tag, &BM_SliceSqrt<T, 7>)}) {
auto* b = benchmark::RegisterBenchmark(entry.first, entry.second);
for (int c : {64, 1024, 4096}) b->Arg(c);
}
}
int RegisterAll() {
add_trait_context<float>("f32");
add_trait_context<double>("f64");
@@ -554,6 +634,10 @@ int RegisterAll() {
benchmark::RegisterBenchmark("Chained/Inverse3x3", &BM_Chained_Inverse3x3);
benchmark::RegisterBenchmark("Chained/Camera", &BM_Chained_Camera);
benchmark::RegisterBenchmark("Chained/Block23", &BM_Chained_Block23);
benchmark::RegisterBenchmark("Chained/Rotation", &BM_Chained_Rotation);
add_slice<float>();
add_slice<double>();
return 0;
}