Representative interview topic

C++ Coding Interview: How Would You Write Portable Vectorized Code with C++26 std::simd?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

Implement an element-wise transform over N floats, preferably with C++26 std::simd, while remaining correct when N is not a vector-width multiple or the runtime hardware is unsuitable. Explain masks, tails, alignment, exceptional values, compiler support, and performance proof.

Prompt and context

Implement y[i] = a * x[i] + b over an array where N may not be a multiple of the vector width and inputs may contain NaN. Use C++26 data-parallel types and explain tails, alignment, masks, compiler support, scalar fallback, and how you prove the optimization helps.

What the interviewer tests

  • Whether you know std::simd is a portable data-parallel abstraction, not a guarantee of one fixed instruction.
  • Whether you distinguish vector values, masks, fixed size, and native width, and handle a partial final batch.
  • Whether you avoid misaligned access assumptions, aliasing violations, out-of-bounds access, and incorrect NaN treatment.
  • Whether you prepare feature testing and fallback paths while C++26 and toolchain support are still uneven.
  • Whether you use benchmarks, correctness tests, and hardware counters instead of claiming speed from source code alone.

Clarifying questions

  1. Does the target compiler and standard library implement the C++26 simd header, or only an experimental namespace?
  2. May input and output alias, and what are the NaN, infinity, and rounding requirements?
  3. What are the N range, element type, error tolerance, and target CPU/GPU instruction sets?
  4. Is the loop memory-bandwidth bound, and is its call frequency high enough to justify vectorization complexity?
  5. Is a fixed ABI width required, or may the implementation choose native width for the target hardware?

30-second answer framework

“I would confirm the simd header implementation and numeric contract first, then load a batch with std::simd, apply the multiply-add, and store it. The main loop handles full vectors; a mask or scalar loop handles the tail without out-of-bounds access. The load tag must match the actual alignment rather than an assumed cast. I would keep a scalar reference and select C++26, an experimental implementation, or scalar fallback with feature tests and a build matrix. Finally I would test NaN, error, and boundary behavior on identical inputs and compare throughput and bandwidth with a fixed benchmark and hardware counters.”

Step-by-step deep dive

1. Choose the data-parallel abstraction

C++26 data-parallel types provide vector values and masks to express one operation over multiple elements. The lane count of std::simd is selected by the implementation and target hardware; choose a fixed-size type only when layout or interface stability requires it. The standard abstraction lets the compiler map to SIMD registers or another suitable implementation.

2. Write the full-batch loop

Let V be the vector type and process complete batches from i through i + V::size(). Input and output should not overlap unless the function contract permits in-place operation, and callers must provide valid ranges. Do not assume arbitrary pointers are aligned; the load tag must match the real alignment guarantee.

cpp
template<class V>
void axpb_simd(const float* x, float* y, std::size_t n, float a, float b) {
  const V va(a), vb(b);
  std::size_t i = 0;
  for (; i + V::size() <= n; i += V::size()) {
    V vx(&x[i], std::element_aligned_tag{});
    (vx * va + vb).copy_to(&y[i], std::element_aligned_tag{});
  }
  for (; i < n; ++i) y[i] = a * x[i] + b;
}

3. Use a mask for the tail

A scalar tail loop is easiest to audit. If tails are common, create an active mask and load and store only valid lanes. The mask must constrain both reads and writes so inactive lanes cannot cause out-of-bounds access or side effects. Do not sacrifice boundary clarity to remove a few scalar iterations.

4. Define NaN, error, and exception semantics

Define NaN propagation, infinity, and rounding requirements before vectorizing. Vectorization can change operation order, so scalar and vector results are not automatically bitwise identical; tests should compare allowed error and special-value behavior. If the business contract requires strict IEEE results or exception-flag order, verify compiler floating-point options and library semantics before choosing SIMD.

5. Provide standardized and fallback paths

The C++26 simd header’s feature-test macro is __cpp_lib_simd, but toolchain support may lag. After capability detection, the build should select standard types, an implementation’s experimental interface, or a scalar template. Do not expose a compiler-private vector type in a public API. Every path should run the same correctness tests.

6. Prove performance and limits

Fix N, data distribution, compiler options, and thread count in benchmarks comparing scalar, SIMD, and different widths. Record throughput, latency, cache misses, vector-instruction ratio, and memory bandwidth; test N=1,000, empty input, an unaligned address, and NaN data separately. If memory bandwidth or call overhead is the bottleneck, SIMD may not help and the simple implementation should remain.

Strong sample answer

“I treat std::simd as a portable data-parallel abstraction and do not assume a fixed lane count or instruction. The main loop processes V::size() full batches and uses element_aligned_tag for arrays that may be unaligned; the tail uses a scalar loop or a mask that constrains reads and writes. I define aliasing, NaN propagation, and error requirements first, then select C++26, an experimental implementation, or a scalar template with feature tests. I test N=1,000, empty input, unaligned addresses, and NaN, and compare throughput, cache behavior, and bandwidth with hardware counters. If memory limits the loop, I do not force vectorization.”

Common mistakes

  • Treat std::simd as a fixed-width register → code depends on one CPU → use implementation-selected or explicitly fixed width deliberately.
  • Load a full vector for the tail → out-of-bounds access → use a mask or scalar tail loop.
  • Assume alignment → undefined behavior or slower loads → match the load tag to the real guarantee.
  • Compare only average values → NaN, infinity, and rounding differences disappear → define special-value and error contracts.
  • Declare victory after seeing vector instructions → memory bandwidth may dominate → prove it with fixed benchmarks and counters.

Follow-ups and responses

How do you choose fixed versus native width?

Native width lets the implementation choose a register width for the target hardware and usually suits throughput. Fixed width suits stable layout, ABI, or reproducible cross-platform behavior. Check the ABI, data layout, and benchmark before choosing; fixed width is not itself a performance guarantee.

Why not always use a mask for the tail?

A mask keeps one loop shape but may add construction, load, and store overhead. A short tail is easier to audit with scalar code; compare measured mask and scalar tails when the tail ratio is material.

How do you prove the compiler did not scalarize the loop?

Inspect generated assembly or optimization reports and pair them with hardware counters for vector instructions, throughput, and cache behavior. Source inspection or one wall-clock sample is insufficient; hold compiler, target flags, and data size constant.

What if the simd header is unavailable in the production compiler?

Use feature tests and a build matrix to select a supported implementation while retaining scalar correctness. Do not copy a private vector type into the public interface. Enable the standard path after a toolchain upgrade and reuse the same boundary and numeric tests.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool