Core: Vectorize complex component reductions

libeigen/eigen!3108

Closes #3097

Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
diff --git a/Eigen/src/Core/CoreEvaluators.h b/Eigen/src/Core/CoreEvaluators.h
index d4f351f..ff9a37f 100644
--- a/Eigen/src/Core/CoreEvaluators.h
+++ b/Eigen/src/Core/CoreEvaluators.h
@@ -1169,6 +1169,100 @@
   Data m_d;
 };
 
+// Read-only component evaluators can gather from contiguous std::complex storage. In particular,
+// PacketAccessBit on a custom expression does not imply array-oriented component access.
+template <typename XprType, int Component,
+          bool Vectorizable = bool(traits<typename XprType::NestedExpression>::Flags & DirectAccessBit) &&
+                              inner_stride_at_compile_time<typename XprType::NestedExpression>::value == 1 &&
+                              (std::is_same<typename XprType::Scalar, float>::value ||
+                               std::is_same<typename XprType::Scalar, double>::value) &&
+                              packet_traits<typename XprType::Scalar>::Vectorizable &&
+                              std::is_same<typename XprType::Scalar, typename unpacket_traits<typename packet_traits<
+                                                                         typename XprType::Scalar>::type>::type>::value>
+struct complex_component_evaluator : unary_evaluator<XprType> {
+  using Base = unary_evaluator<XprType>;
+  EIGEN_DEVICE_FUNC constexpr EIGEN_STRONG_INLINE explicit complex_component_evaluator(const XprType& xpr)
+      : Base(xpr) {}
+};
+
+template <typename XprType, int Component>
+struct complex_component_evaluator<XprType, Component, true> : unary_evaluator<XprType> {
+  using Base = unary_evaluator<XprType>;
+  using Scalar = typename XprType::Scalar;
+  using ArgType = typename XprType::NestedExpression;
+  // Blocks must delegate packet reads here rather than reinterpret this stride-2 view as a contiguous Map.
+  static constexpr unsigned int Flags = (Base::Flags & ~DirectAccessBit) | PacketAccessBit;
+  static constexpr int Alignment = 0;
+
+  EIGEN_DEVICE_FUNC constexpr EIGEN_STRONG_INLINE explicit complex_component_evaluator(const XprType& xpr)
+      : Base(xpr),
+        m_data(reinterpret_cast<const Scalar*>(xpr.nestedExpression().data())),
+        m_outerStride(xpr.nestedExpression().outerStride()) {}
+
+  template <int LoadMode, typename PacketType>
+  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketType packet(Index index) const {
+    return pgather<Scalar, PacketType>(m_data + 2 * index + Component, 2);
+  }
+
+  template <int LoadMode, typename PacketType>
+  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketType packet(Index row, Index col) const {
+    const Index index = XprType::IsRowMajor ? row * m_outerStride.value() + col : col * m_outerStride.value() + row;
+    return packet<LoadMode, PacketType>(index);
+  }
+
+  template <int LoadMode, typename PacketType>
+  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketType packetSegment(Index index, Index begin, Index count) const {
+    Scalar values[unpacket_traits<PacketType>::size] = {};
+    for (Index i = begin; i < begin + count; ++i) values[i] = Base::coeff(index + i);
+    return ploadu<PacketType>(values);
+  }
+
+  template <int LoadMode, typename PacketType>
+  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE PacketType packetSegment(Index row, Index col, Index begin, Index count) const {
+    Scalar values[unpacket_traits<PacketType>::size] = {};
+    for (Index i = begin; i < begin + count; ++i)
+      values[i] = Base::coeff(row + (XprType::IsRowMajor ? 0 : i), col + (XprType::IsRowMajor ? i : 0));
+    return ploadu<PacketType>(values);
+  }
+
+ private:
+  const Scalar* m_data;
+  const variable_if_dynamic<Index, outer_stride_at_compile_time<ArgType>::value> m_outerStride;
+};
+
+template <typename Real, typename ArgType>
+struct evaluator<CwiseUnaryOp<scalar_real_op<std::complex<Real>>, ArgType>>
+    : complex_component_evaluator<CwiseUnaryOp<scalar_real_op<std::complex<Real>>, ArgType>, 0> {
+  using XprType = CwiseUnaryOp<scalar_real_op<std::complex<Real>>, ArgType>;
+  using Base = complex_component_evaluator<XprType, 0>;
+  EIGEN_DEVICE_FUNC constexpr EIGEN_STRONG_INLINE explicit evaluator(const XprType& xpr) : Base(xpr) {}
+};
+
+// Writable views keep scalar stores: assignment functors require contiguous destination packets.
+template <typename Real, typename ArgType>
+struct evaluator<const CwiseUnaryView<scalar_real_ref_op<std::complex<Real>>, ArgType, Stride<0, 0>>>
+    : complex_component_evaluator<CwiseUnaryView<scalar_real_ref_op<std::complex<Real>>, ArgType, Stride<0, 0>>, 0> {
+  using XprType = CwiseUnaryView<scalar_real_ref_op<std::complex<Real>>, ArgType, Stride<0, 0>>;
+  using Base = complex_component_evaluator<XprType, 0>;
+  EIGEN_DEVICE_FUNC constexpr EIGEN_STRONG_INLINE explicit evaluator(const XprType& xpr) : Base(xpr) {}
+};
+
+template <typename Real, typename ArgType>
+struct evaluator<CwiseUnaryOp<scalar_imag_op<std::complex<Real>>, ArgType>>
+    : complex_component_evaluator<CwiseUnaryOp<scalar_imag_op<std::complex<Real>>, ArgType>, 1> {
+  using XprType = CwiseUnaryOp<scalar_imag_op<std::complex<Real>>, ArgType>;
+  using Base = complex_component_evaluator<XprType, 1>;
+  EIGEN_DEVICE_FUNC constexpr EIGEN_STRONG_INLINE explicit evaluator(const XprType& xpr) : Base(xpr) {}
+};
+
+template <typename Real, typename ArgType>
+struct evaluator<const CwiseUnaryView<scalar_imag_ref_op<std::complex<Real>>, ArgType, Stride<0, 0>>>
+    : complex_component_evaluator<CwiseUnaryView<scalar_imag_ref_op<std::complex<Real>>, ArgType, Stride<0, 0>>, 1> {
+  using XprType = CwiseUnaryView<scalar_imag_ref_op<std::complex<Real>>, ArgType, Stride<0, 0>>;
+  using Base = complex_component_evaluator<XprType, 1>;
+  EIGEN_DEVICE_FUNC constexpr EIGEN_STRONG_INLINE explicit evaluator(const XprType& xpr) : Base(xpr) {}
+};
+
 // -------------------- Map --------------------
 
 // FIXME: consider using Derived::PlainObject for PlainObjectType.
@@ -1325,7 +1419,7 @@
 // -------------------- Block --------------------
 
 template <typename ArgType, int BlockRows, int BlockCols, bool InnerPanel,
-          bool HasDirectAccess = has_direct_access<ArgType>::value>
+          bool HasDirectAccess = has_direct_access<ArgType>::value && bool(evaluator<ArgType>::Flags & DirectAccessBit)>
 struct block_evaluator;
 
 template <typename ArgType, int BlockRows, int BlockCols, bool InnerPanel>
@@ -1354,7 +1448,11 @@
                                                             : int(outer_stride_at_compile_time<ArgType>::value),
     OuterStrideAtCompileTime = HasSameStorageOrderAsArgType ? int(outer_stride_at_compile_time<ArgType>::value)
                                                             : int(inner_stride_at_compile_time<ArgType>::value),
-    MaskPacketAccessBit = (InnerStrideAtCompileTime == 1 || HasSameStorageOrderAsArgType) ? PacketAccessBit : 0,
+    // Direct-access blocks use mapbase_evaluator's contiguous loads, not the parent's packet method.
+    MaskPacketAccessBit = (InnerStrideAtCompileTime == 1 ||
+                           (!(evaluator<ArgType>::Flags & DirectAccessBit) && HasSameStorageOrderAsArgType))
+                              ? PacketAccessBit
+                              : 0,
 
     FlagsLinearAccessBit = (RowsAtCompileTime == 1 || ColsAtCompileTime == 1 ||
                             (InnerPanel && (evaluator<ArgType>::Flags & LinearAccessBit)))
diff --git a/Eigen/src/Core/Redux.h b/Eigen/src/Core/Redux.h
index c19a00a..133cbdc 100644
--- a/Eigen/src/Core/Redux.h
+++ b/Eigen/src/Core/Redux.h
@@ -492,8 +492,8 @@
 
 // evaluator adaptor
 template <typename XprType_>
-class redux_evaluator : public internal::evaluator<XprType_> {
-  using Base = internal::evaluator<XprType_>;
+class redux_evaluator : public internal::evaluator<const XprType_> {
+  using Base = internal::evaluator<const XprType_>;
 
  public:
   using XprType = XprType_;
diff --git a/Eigen/src/Core/arch/AVX/PacketMath.h b/Eigen/src/Core/arch/AVX/PacketMath.h
index e7a4dc4..129c8e0 100644
--- a/Eigen/src/Core/arch/AVX/PacketMath.h
+++ b/Eigen/src/Core/arch/AVX/PacketMath.h
@@ -1638,11 +1638,32 @@
 // 4);
 template <>
 EIGEN_DEVICE_FUNC inline Packet8f pgather<float, Packet8f>(const float* from, Index stride) {
+  if (stride == 2) {
+#ifdef EIGEN_VECTORIZE_AVX2
+    // Both loads stay within from[0..14], including when from points to an imaginary component.
+    const Packet8f low = _mm256_loadu_ps(from);
+    const Packet8f high = _mm256_loadu_ps(from + 7);
+    const Packet8f interleaved = _mm256_shuffle_ps(low, high, _MM_SHUFFLE(3, 1, 2, 0));
+    return _mm256_castpd_ps(_mm256_permute4x64_pd(_mm256_castps_pd(interleaved), _MM_SHUFFLE(3, 1, 2, 0)));
+#else
+    const Packet4f low = pgather<float, Packet4f>(from, 2);
+    const Packet4f high = pgather<float, Packet4f>(from + 8, 2);
+    return _mm256_insertf128_ps(_mm256_castps128_ps256(low), high, 1);
+#endif
+  }
   return _mm256_set_ps(from[7 * stride], from[6 * stride], from[5 * stride], from[4 * stride], from[3 * stride],
                        from[2 * stride], from[1 * stride], from[0 * stride]);
 }
 template <>
 EIGEN_DEVICE_FUNC inline Packet4d pgather<double, Packet4d>(const double* from, Index stride) {
+#ifdef EIGEN_VECTORIZE_AVX2
+  if (stride == 2) {
+    const Packet4d low = _mm256_loadu_pd(from);
+    const Packet4d high = _mm256_loadu_pd(from + 3);
+    const Packet4d interleaved = _mm256_shuffle_pd(low, high, 0xa);
+    return _mm256_permute4x64_pd(interleaved, _MM_SHUFFLE(3, 1, 2, 0));
+  }
+#endif
   return _mm256_set_pd(from[3 * stride], from[2 * stride], from[1 * stride], from[0 * stride]);
 }
 template <>
diff --git a/Eigen/src/Core/arch/AVX512/PacketMath.h b/Eigen/src/Core/arch/AVX512/PacketMath.h
index 3f1ba2d..2596313 100644
--- a/Eigen/src/Core/arch/AVX512/PacketMath.h
+++ b/Eigen/src/Core/arch/AVX512/PacketMath.h
@@ -1247,6 +1247,13 @@
 
 template <>
 EIGEN_DEVICE_FUNC inline Packet16f pgather<float, Packet16f>(const float* from, Index stride) {
+  if (stride == 2) {
+    // The overlapping loads end at from[30], not the unused coefficient from[31].
+    const Packet16f low = _mm512_loadu_ps(from);
+    const Packet16f high = _mm512_loadu_ps(from + 15);
+    const Packet16i indices = _mm512_setr_epi32(0, 2, 4, 6, 8, 10, 12, 14, 17, 19, 21, 23, 25, 27, 29, 31);
+    return _mm512_permutex2var_ps(low, indices, high);
+  }
   Packet16i stride_vector = _mm512_set1_epi32(convert_index<int>(stride));
   Packet16i stride_multiplier = _mm512_set_epi32(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);
   Packet16i indices = _mm512_mullo_epi32(stride_vector, stride_multiplier);
@@ -1255,6 +1262,12 @@
 }
 template <>
 EIGEN_DEVICE_FUNC inline Packet8d pgather<double, Packet8d>(const double* from, Index stride) {
+  if (stride == 2) {
+    const Packet8d low = _mm512_loadu_pd(from);
+    const Packet8d high = _mm512_loadu_pd(from + 7);
+    const __m512i indices = _mm512_setr_epi64(0, 2, 4, 6, 9, 11, 13, 15);
+    return _mm512_permutex2var_pd(low, indices, high);
+  }
   Packet8i stride_vector = _mm256_set1_epi32(convert_index<int>(stride));
   Packet8i stride_multiplier = _mm256_set_epi32(7, 6, 5, 4, 3, 2, 1, 0);
   Packet8i indices = _mm256_mullo_epi32(stride_vector, stride_multiplier);
diff --git a/Eigen/src/Core/arch/SSE/PacketMath.h b/Eigen/src/Core/arch/SSE/PacketMath.h
index 4e91e4d..ff3d13a 100644
--- a/Eigen/src/Core/arch/SSE/PacketMath.h
+++ b/Eigen/src/Core/arch/SSE/PacketMath.h
@@ -1567,6 +1567,10 @@
 
 template <>
 EIGEN_STRONG_INLINE Packet4f pgather<float, Packet4f>(const float* from, Index stride) {
+  if (stride == 2) {
+    // Overlap the loads so the second ends at the last gathered coefficient, from[6].
+    return _mm_shuffle_ps(_mm_loadu_ps(from), _mm_loadu_ps(from + 3), _MM_SHUFFLE(3, 1, 2, 0));
+  }
   return _mm_set_ps(from[3 * stride], from[2 * stride], from[1 * stride], from[0 * stride]);
 }
 template <>
diff --git a/benchmarks/Core/CMakeLists.txt b/benchmarks/Core/CMakeLists.txt
index 67db020..1da6867 100644
--- a/benchmarks/Core/CMakeLists.txt
+++ b/benchmarks/Core/CMakeLists.txt
@@ -22,6 +22,8 @@
 eigen_add_benchmark(bench_move_semantics bench_move_semantics.cpp)
 eigen_add_benchmark(bench_reductions bench_reductions.cpp)
 eigen_add_benchmark(bench_vectorwise_norm bench_vectorwise_norm.cpp)
+eigen_add_benchmark(bench_complex_components bench_complex_components.cpp)
+eigen_add_benchmark(bench_pgather bench_pgather.cpp)
 eigen_add_benchmark(bench_boolean_reductions bench_boolean_reductions.cpp)
 eigen_add_benchmark(bench_count bench_count.cpp)
 eigen_add_benchmark(bench_dot bench_dot.cpp)
diff --git a/benchmarks/Core/bench_complex_components.cpp b/benchmarks/Core/bench_complex_components.cpp
new file mode 100644
index 0000000..3b5c6b8
--- /dev/null
+++ b/benchmarks/Core/bench_complex_components.cpp
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: The Eigen Authors
+// SPDX-License-Identifier: MPL-2.0
+
+#include <benchmark/benchmark.h>
+#include <Eigen/Core>
+
+using namespace Eigen;
+
+template <bool Imaginary, int NaNPropagation, typename Vector>
+EIGEN_DONT_INLINE typename Vector::RealScalar component_abs_max(Vector& values) {
+  if (Imaginary) return values.imag().cwiseAbs().template maxCoeff<NaNPropagation>();
+  return values.real().cwiseAbs().template maxCoeff<NaNPropagation>();
+}
+
+template <typename Real, bool Imaginary, bool Const, int NaNPropagation>
+static void BM_ComponentAbsMax(benchmark::State& state) {
+  using Complex = std::complex<Real>;
+  using Vector = Matrix<Complex, Dynamic, 1>;
+  Vector values = Vector::Random(state.range(0));
+  std::conditional_t<Const, const Vector, Vector>& input = values;
+  Real expected = 0;
+  for (Index i = 0; i < values.size(); ++i) {
+    const Real value = numext::abs(Imaginary ? values(i).imag() : values(i).real());
+    if (value > expected) expected = value;
+  }
+  if (component_abs_max<Imaginary, NaNPropagation>(input) != expected) {
+    state.SkipWithError("Incorrect component maximum");
+    return;
+  }
+  for (auto _ : state) {
+    benchmark::ClobberMemory();
+    benchmark::DoNotOptimize(component_abs_max<Imaginary, NaNPropagation>(input));
+  }
+  state.SetBytesProcessed(state.iterations() * values.size() * sizeof(Complex));
+}
+
+// Small packet boundaries, the issue's cache-resident workload, and larger working sets.
+#define COMPONENT_SIZES ->Arg(3)->Arg(8)->Arg(9)->Arg(64)->Arg(4096)->Arg(65536)
+#define REGISTER_COMPONENT(Real, Imaginary, Const, Policy) \
+  BENCHMARK_TEMPLATE(BM_ComponentAbsMax, Real, Imaginary, Const, Policy) COMPONENT_SIZES
+#define REGISTER_TYPE(Real)                              \
+  REGISTER_COMPONENT(Real, false, false, PropagateFast); \
+  REGISTER_COMPONENT(Real, false, false, PropagateNaN);  \
+  REGISTER_COMPONENT(Real, false, true, PropagateNaN);   \
+  REGISTER_COMPONENT(Real, true, false, PropagateNaN);   \
+  REGISTER_COMPONENT(Real, true, true, PropagateNaN)
+REGISTER_TYPE(float);
+REGISTER_TYPE(double);
+#undef REGISTER_TYPE
+#undef REGISTER_COMPONENT
+#undef COMPONENT_SIZES
diff --git a/benchmarks/Core/bench_pgather.cpp b/benchmarks/Core/bench_pgather.cpp
new file mode 100644
index 0000000..38c7894
--- /dev/null
+++ b/benchmarks/Core/bench_pgather.cpp
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: The Eigen Authors
+// SPDX-License-Identifier: MPL-2.0
+
+#include <benchmark/benchmark.h>
+#include <Eigen/Core>
+
+using namespace Eigen;
+
+template <typename Scalar, int Stride>
+EIGEN_DONT_INLINE void gather_packets(const Scalar* input, Scalar* output, Index size, Index runtime_stride) {
+  using Packet = typename internal::packet_traits<Scalar>::type;
+  constexpr Index PacketSize = internal::packet_traits<Scalar>::size;
+  const Index stride = Stride == Dynamic ? runtime_stride : Stride;
+  for (Index i = 0; i < size; i += PacketSize) {
+    internal::pstoreu(output + i, internal::pgather<Scalar, Packet>(input + i * stride, stride));
+  }
+}
+
+template <typename Scalar, int Stride>
+static void BM_Pgather(benchmark::State& state) {
+  const Index size = state.range(0);
+  const Index stride = Stride == Dynamic ? state.range(1) : Stride;
+  Array<Scalar, Dynamic, 1> input((size - 1) * stride + 1), output(size);
+  for (Index i = 0; i < input.size(); ++i) input(i) = Scalar(i % 127);
+  gather_packets<Scalar, Stride>(input.data(), output.data(), size, stride);
+  for (Index i = 0; i < size; ++i) {
+    if (output(i) != input(i * stride)) {
+      state.SkipWithError("Incorrect gathered coefficient");
+      return;
+    }
+  }
+  for (auto _ : state) {
+    benchmark::ClobberMemory();
+    gather_packets<Scalar, Stride>(input.data(), output.data(), size, stride);
+    benchmark::DoNotOptimize(output.data());
+  }
+  state.SetItemsProcessed(state.iterations() * size);
+}
+
+BENCHMARK_TEMPLATE(BM_Pgather, float, 2)->Arg(64)->Arg(4096);
+BENCHMARK_TEMPLATE(BM_Pgather, double, 2)->Arg(64)->Arg(4096);
+BENCHMARK_TEMPLATE(BM_Pgather, float, Dynamic)->ArgsProduct({{64, 4096}, {1, 2, 3, 7}});
+BENCHMARK_TEMPLATE(BM_Pgather, double, Dynamic)->ArgsProduct({{64, 4096}, {1, 2, 3, 7}});
diff --git a/benchmarks/Core/bench_reductions.cpp b/benchmarks/Core/bench_reductions.cpp
index e09f69e..16459d6 100644
--- a/benchmarks/Core/bench_reductions.cpp
+++ b/benchmarks/Core/bench_reductions.cpp
@@ -79,8 +79,7 @@
   state.SetBytesProcessed(state.iterations() * n * sizeof(Scalar));
 }
 
-// real() builds a CwiseUnaryView, which drops PacketAccessBit, so this reduces coefficient by
-// coefficient and reaches the scalar form of the wrapper.
+// Component reductions can use stride-2 packet gathers.
 template <typename Scalar, int NaNPropagation>
 static void BM_ComplexRealAbsMaxCoeff(benchmark::State& state) {
   using Real = typename NumTraits<Scalar>::Real;
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 79da7f4..553fb80 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -269,6 +269,7 @@
 ei_add_test(clz)
 ei_add_test(rand)
 ei_add_test(realview)
+ei_add_test(complex_components)
 ei_add_test(meta)
 ei_add_test(preprocessor_directives)
 ei_add_test(lru_cache)
@@ -292,6 +293,7 @@
 ei_add_test(float_conversion)
 ei_add_test(io)
 ei_add_test(packetmath "-DEIGEN_FAST_MATH=1")
+ei_add_test(packetmath_gather)
 # Generic clang vector backend tests for different vector sizes.
 include(CheckCXXSourceCompiles)
 check_cxx_source_compiles("
diff --git a/test/complex_components.cpp b/test/complex_components.cpp
new file mode 100644
index 0000000..c6bef59
--- /dev/null
+++ b/test/complex_components.cpp
@@ -0,0 +1,177 @@
+// SPDX-FileCopyrightText: The Eigen Authors
+// SPDX-License-Identifier: MPL-2.0
+
+#include "main.h"
+#include <Eigen/Core>
+
+// Component access must not assume that a user-defined complex scalar has std::complex's layout.
+struct PaddedComplex : std::complex<double> {
+  using std::complex<double>::complex;
+  double padding = 123;
+};
+
+template <>
+struct Eigen::NumTraits<PaddedComplex> : Eigen::NumTraits<std::complex<double>> {};
+
+template <typename Derived>
+void check_component(const DenseBase<Derived>& component) {
+  using Real = typename Derived::Scalar;
+  Matrix<Real, Dynamic, Dynamic, Derived::IsRowMajor ? RowMajor : ColMajor> copy = component;
+  Real sum = 0, minimum = component(0, 0), maximum = minimum, abs_maximum = numext::abs(minimum);
+  for (Index col = 0; col < component.cols(); ++col) {
+    for (Index row = 0; row < component.rows(); ++row) {
+      const Real value = component(row, col);
+      VERIFY_IS_EQUAL(copy(row, col), value);
+      sum += value;
+      if (value < minimum) minimum = value;
+      if (value > maximum) maximum = value;
+      if (numext::abs(value) > abs_maximum) abs_maximum = numext::abs(value);
+    }
+  }
+  VERIFY_IS_EQUAL(component.sum(), sum);
+  VERIFY_IS_EQUAL(component.template minCoeff<PropagateNaN>(), minimum);
+  VERIFY_IS_EQUAL(component.template maxCoeff<PropagateNaN>(), maximum);
+  VERIFY_IS_EQUAL(component.derived().cwiseAbs().template maxCoeff<PropagateNaN>(), abs_maximum);
+  VERIFY_IS_CWISE_EQUAL(component.derived().colwise().sum().eval(), copy.colwise().sum().eval());
+  VERIFY_IS_CWISE_EQUAL(component.derived().rowwise().sum().eval(), copy.rowwise().sum().eval());
+}
+
+template <typename MatrixType>
+void component_expressions(Index rows, Index cols) {
+  using Complex = typename MatrixType::Scalar;
+  using Real = typename NumTraits<Complex>::Real;
+  MatrixType values(rows, cols);
+  for (Index i = 0; i < values.size(); ++i) values.data()[i] = Complex(Real(i % 13 - 6), Real(i % 17 + 1));
+  const MatrixType& const_values = values;
+  check_component(values.real());
+  check_component(values.imag());
+  check_component(const_values.real());
+  check_component(const_values.imag());
+  check_component(values.transpose().real());
+  check_component(values.transpose().imag());
+  check_component(values.conjugate().real());
+  check_component(values.conjugate().imag());
+  check_component(values.block(0, 0, rows - 1, cols - 1).real());
+  check_component(values.block(0, 0, rows - 1, cols - 1).imag());
+  check_component(const_values.block(0, 0, rows - 1, cols - 1).real());
+  check_component(const_values.block(0, 0, rows - 1, cols - 1).imag());
+  const auto real = values.real();
+  const auto imag = values.imag();
+  check_component(real.block(0, 0, rows - 1, cols - 1));
+  check_component(imag.block(0, 0, rows - 1, cols - 1));
+
+  MatrixType original = values;
+  values.real() = original.imag();
+  values.imag() = original.real();
+  for (Index i = 0; i < values.size(); ++i) {
+    VERIFY_IS_EQUAL(values.data()[i], Complex(original.data()[i].imag(), original.data()[i].real()));
+  }
+  values.real().block(0, 0, rows - 1, cols - 1).setConstant(Real(3));
+  values.imag().array() += Real(2);
+  for (Index col = 0; col < cols; ++col) {
+    for (Index row = 0; row < rows; ++row) {
+      VERIFY_IS_EQUAL(values(row, col).imag(), original(row, col).real() + Real(2));
+      VERIFY_IS_EQUAL(values(row, col).real(), row < rows - 1 && col < cols - 1 ? Real(3) : original(row, col).imag());
+    }
+  }
+}
+
+template <typename Real>
+void component_reductions() {
+  using Complex = std::complex<Real>;
+  using Vector = Matrix<Complex, Dynamic, 1>;
+  constexpr bool vectorizable = (std::is_same<Real, float>::value || std::is_same<Real, double>::value) &&
+                                internal::packet_traits<Real>::Vectorizable;
+  using View = decltype(std::declval<Vector&>().real());
+  using ConstOp = decltype(std::declval<const Vector&>().imag());
+  STATIC_CHECK(bool(internal::redux_evaluator<View>::Flags & PacketAccessBit) == vectorizable);
+  STATIC_CHECK(bool(internal::evaluator<ConstOp>::Flags & PacketAccessBit) == vectorizable);
+  STATIC_CHECK(!(internal::evaluator<View>::Flags & PacketAccessBit));
+  using Strided = Map<Vector, Unaligned, InnerStride<2>>;
+  using StridedView = decltype(std::declval<Strided&>().real());
+  STATIC_CHECK(!(internal::redux_evaluator<StridedView>::Flags & PacketAccessBit));
+  using DynamicStride = Map<Vector, Unaligned, InnerStride<Dynamic>>;
+  using DynamicView = decltype(std::declval<const DynamicStride&>().imag());
+  STATIC_CHECK(!(internal::evaluator<DynamicView>::Flags & PacketAccessBit));
+  using SumView = decltype((std::declval<Vector>() + std::declval<Vector>()).real());
+  STATIC_CHECK(!(internal::evaluator<SumView>::Flags & PacketAccessBit));
+
+  const Real nan = NumTraits<Real>::quiet_NaN();
+  const Real inf = NumTraits<Real>::infinity();
+  constexpr Index packet_size = internal::packet_traits<Real>::size;
+  for (Index size : {Index(1), packet_size, packet_size + 1, 2 * packet_size - 1, Index(65)}) {
+    Vector buffer(2 * size + 1);
+    buffer.setConstant(Complex(Real(7), Real(-3)));
+    Map<Vector> values(buffer.data() + 1, size);
+    const auto& const_values = values;
+    check_component(values.real());
+    check_component(values.imag());
+    Strided strided(buffer.data() + 1, size);
+    check_component(strided.real());
+    check_component(strided.imag());
+    DynamicStride dynamic(buffer.data() + 1, size, InnerStride<Dynamic>(2));
+    check_component(dynamic.real());
+    check_component(dynamic.imag());
+    for (Index i = 0; i < size; ++i) {
+      for (Real special : {nan, inf, -inf, Real(0), -Real(0), std::numeric_limits<Real>::denorm_min()}) {
+        values.setConstant(Complex(Real(7), Real(-3)));
+        values(i) = Complex(special, Real(-3));
+        Matrix<Real, Dynamic, 1> real = const_values.real();
+        if ((numext::isnan)(special)) {
+          VERIFY((numext::isnan)(real(i)));
+          VERIFY((numext::isnan)(values.real().template maxCoeff<PropagateNaN>()));
+          VERIFY((numext::isnan)(const_values.real().template minCoeff<PropagateNaN>()));
+          if (size > 1) VERIFY_IS_EQUAL(values.real().template maxCoeff<PropagateNumbers>(), Real(7));
+        } else {
+          VERIFY_IS_EQUAL(real(i), special);
+          VERIFY_IS_EQUAL((std::signbit)(real(i)), (std::signbit)(special));
+        }
+        VERIFY_IS_EQUAL(values.imag().template maxCoeff<PropagateNaN>(), Real(-3));
+        values(i) = Complex(Real(7), special);
+        Matrix<Real, Dynamic, 1> imag = const_values.imag();
+        if ((numext::isnan)(special)) {
+          VERIFY((numext::isnan)(imag(i)));
+          VERIFY((numext::isnan)(values.imag().template minCoeff<PropagateNaN>()));
+          VERIFY((numext::isnan)(const_values.imag().template maxCoeff<PropagateNaN>()));
+          if (size > 1) VERIFY_IS_EQUAL(values.imag().template minCoeff<PropagateNumbers>(), Real(-3));
+        } else {
+          VERIFY_IS_EQUAL(imag(i), special);
+          VERIFY_IS_EQUAL((std::signbit)(imag(i)), (std::signbit)(special));
+        }
+        VERIFY_IS_EQUAL(values.real().template minCoeff<PropagateNaN>(), Real(7));
+      }
+    }
+  }
+}
+
+void component_custom_scalar() {
+  using Vector = Matrix<PaddedComplex, Dynamic, 1>;
+  Vector values(17);
+  for (Index i = 0; i < values.size(); ++i) values(i) = PaddedComplex(double(i), double(2 * i + 1));
+  const Vector& const_values = values;
+  using RealView = decltype(const_values.real());
+  using ImagView = decltype(const_values.imag());
+  STATIC_CHECK(!(internal::evaluator<RealView>::Flags & PacketAccessBit));
+  STATIC_CHECK(!(internal::evaluator<ImagView>::Flags & PacketAccessBit));
+  check_component(const_values.real());
+  check_component(const_values.imag());
+}
+
+EIGEN_DECLARE_TEST(complex_components) {
+  CALL_SUBTEST_1(component_reductions<float>());
+  CALL_SUBTEST_2(component_reductions<double>());
+  CALL_SUBTEST_3(component_reductions<long double>());
+  CALL_SUBTEST_3(component_custom_scalar());
+  for (Index rows : {Index(2), Index(7), Index(17)}) {
+    for (Index cols : {Index(3), Index(9)}) {
+      EIGEN_UNUSED_VARIABLE(rows);
+      EIGEN_UNUSED_VARIABLE(cols);
+      CALL_SUBTEST_1((component_expressions<Matrix<std::complex<float>, Dynamic, Dynamic, ColMajor>>(rows, cols)));
+      CALL_SUBTEST_1((component_expressions<Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>>(rows, cols)));
+      CALL_SUBTEST_2((component_expressions<Matrix<std::complex<double>, Dynamic, Dynamic, ColMajor>>(rows, cols)));
+      CALL_SUBTEST_2((component_expressions<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>(rows, cols)));
+    }
+  }
+  CALL_SUBTEST_1((component_expressions<Matrix<std::complex<float>, 3, 5>>(3, 5)));
+  CALL_SUBTEST_2((component_expressions<Matrix<std::complex<double>, 2, 2>>(2, 2)));
+}
diff --git a/test/packetmath_gather.cpp b/test/packetmath_gather.cpp
new file mode 100644
index 0000000..f88e6c4
--- /dev/null
+++ b/test/packetmath_gather.cpp
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: The Eigen Authors
+// SPDX-License-Identifier: MPL-2.0
+
+#include "packetmath_test_shared.h"
+#include <memory>
+
+namespace Eigen {
+namespace test {
+
+template <typename Scalar, typename Packet>
+struct runall<Scalar, Packet, false, false> {
+  static void run() {
+    constexpr int PacketSize = internal::unpacket_traits<Packet>::size;
+    auto samples = special_values<Scalar>();
+    using Bits = typename numext::get_integer_by_size<sizeof(Scalar)>::unsigned_type;
+    const Bits nan_payload = numext::bit_cast<Bits>(NumTraits<Scalar>::quiet_NaN()) | Bits(0x123);
+    samples.push_back(numext::bit_cast<Scalar>(nan_payload));
+    Scalar actual[PacketSize], expected[PacketSize];
+    for (Index stride : {Index(-7), Index(-2), Index(-1), Index(0), Index(1), Index(2), Index(3), Index(7)}) {
+      const Index span = (PacketSize - 1) * numext::abs(stride) + 1;
+      for (Index offset = 0; offset < PacketSize; ++offset) {
+        // Exact-size new[] lets sanitizers detect reads past the last gathered coefficient.
+        std::unique_ptr<Scalar[]> storage(new Scalar[span + offset]);
+        Scalar* data = storage.get() + offset;
+        const Scalar* from = stride < 0 ? data + span - 1 : data;
+        for (Index rotation = 0; rotation < Index(samples.size()); ++rotation) {
+          for (Index i = 0; i < span; ++i) data[i] = samples[(i + rotation) % samples.size()];
+          for (Index i = 0; i < PacketSize; ++i) expected[i] = from[i * stride];
+          volatile Index runtime_stride = stride;
+          internal::pstoreu(actual, internal::pgather<Scalar, Packet>(from, runtime_stride));
+          VERIFY(areEqualBits(expected, actual, PacketSize, false));
+          if (stride == 2) {
+            internal::pstoreu(actual, internal::pgather<Scalar, Packet>(from, 2));
+            VERIFY(areEqualBits(expected, actual, PacketSize, false));
+          }
+        }
+      }
+    }
+  }
+};
+
+}  // namespace test
+}  // namespace Eigen
+
+EIGEN_DECLARE_TEST(packetmath_gather) {
+  CALL_SUBTEST_1(test::runner<float>::run());
+  CALL_SUBTEST_2(test::runner<double>::run());
+}