Tema representativo de entrevista

Entrevista técnica de C++: ¿Cómo escribirías código vectorizado portable con C++26 std::simd?

CodingDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Implementa una transformación elemento a elemento sobre N flotantes, preferiblemente con C++26 std::simd, manteniéndose correcto cuando N no es un múltiplo del ancho del vector o cuando el hardware en tiempo de ejecución no es adecuado. Explica máscaras, colas (tails), alineación, valores excepcionales, soporte del compilador y prueba de rendimiento.

Prompt y contexto

Implementa y[i] = a * x[i] + b sobre un arreglo donde N puede no ser un múltiplo del ancho del vector y las entradas pueden contener NaN. Utiliza tipos paralelos a nivel de datos de C++26 y explica colas (tails), alineación, máscaras, soporte del compilador, fallback escalar y cómo demuestras que la optimización ayuda.

Qué evalúa el entrevistador

  • Si sabes que std::simd es una abstracción paralela a nivel de datos portable y no la garantía de una instrucción fija.
  • Si distingues entre valores vectoriales, máscaras, tamaño fijo y ancho nativo, y manejas un lote final parcial.
  • Si evitas suposiciones de acceso desalineado, violaciones de aliasing, accesos fuera de límites y un tratamiento incorrecto de NaN.
  • Si preparas pruebas de características (feature testing) y rutas de fallback mientras el soporte de C++26 y de la cadena de herramientas (toolchain) siga siendo dispar.
  • Si utilizas benchmarks, pruebas de corrección y contadores de hardware en lugar de afirmar mejoras de velocidad basándote solo en el código fuente.

Preguntas para clarificar

  1. ¿El compilador de destino y la biblioteca estándar implementan el encabezado simd de C++26 o solo un espacio de nombres experimental?
  2. ¿La entrada y la salida pueden tener aliasing, y cuáles son los requisitos de NaN, infinito y redondeo?
  3. ¿Cuáles son el rango de N, el tipo de elemento, la tolerancia a errores y los conjuntos de instrucciones de CPU/GPU de destino?
  4. ¿El bucle está limitado por el ancho de banda de memoria y su frecuencia de llamada es lo suficientemente alta como para justificar la complejidad de la vectorización?
  5. ¿Se requiere un ancho de ABI fijo o la implementación puede elegir el ancho nativo para el hardware de destino?

Estructura de respuesta de 30 segundos

“Primero confirmaría la implementación del encabezado simd y el contrato numérico, luego cargaría un lote con std::simd, aplicaría la multiplicación-suma y lo almacenaría. El bucle principal maneja vectores completos; una máscara o un bucle escalar maneja la cola sin accesos fuera de límites. La etiqueta de carga debe coincidir con la alineación real en lugar de asumir un cast. Mantendría una referencia escalar y seleccionaría C++26, una implementación experimental o un fallback escalar mediante feature tests y una matriz de compilación. Finalmente, probaría el comportamiento de NaN, errores y límites con entradas idénticas y compararía el throughput y el ancho de banda con un benchmark fijo y contadores de hardware”.

Análisis paso a paso a profundidad

1. Elegir la abstracción paralela a nivel de datos

Los tipos paralelos a nivel de datos de C++26 proporcionan valores vectoriales y máscaras para expresar una operación sobre múltiples elementos. La cantidad de carriles (lanes) de std::simd es seleccionada por la implementación y el hardware de destino; elige un tipo de tamaño fijo solo cuando la estabilidad del layout o de la interfaz lo requiera. La abstracción estándar permite que el compilador mapee a registros SIMD u otra implementación adecuada.

2. Escribir el bucle de lotes completos

Sea V el tipo de vector y procesa lotes completos desde i hasta i + V::size(). La entrada y la salida no deben superponerse a menos que el contrato de la función permita la operación in-place, y quienes llaman a la función deben proporcionar rangos válidos. No asumas que los punteros arbitrarios están alineados; la etiqueta de carga debe coincidir con la garantía de alineación real.

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. Usar una máscara para la cola

Un bucle escalar para la cola es lo más fácil de auditar. Si las colas son comunes, crea una máscara activa y carga y almacena únicamente los carriles válidos. La máscara debe restringir tanto las lecturas como las escrituras para que los carriles inactivos no puedan causar accesos fuera de límites o efectos secundarios. No sacrifiques la claridad en los límites por eliminar unas pocas iteraciones escalares.

4. Definir la semántica de NaN, errores y excepciones

Define la propagación de NaN, el infinito y los requisitos de redondeo antes de vectorizar. La vectorización puede cambiar el orden de las operaciones, por lo que los resultados escalares y vectoriales no son automáticamente idénticos a nivel de bits; las pruebas deben comparar el error permitido y el comportamiento de valores especiales. Si el contrato de negocio requiere resultados estrictos de IEEE o el orden de los flags de excepciones, verifica las opciones de punto flotante del compilador y la semántica de la biblioteca antes de elegir SIMD.

5. Proporcionar rutas estandarizadas y de fallback

La macro de prueba de características (feature-test macro) para el encabezado simd de C++26 es __cpp_lib_simd, pero el soporte de la cadena de herramientas puede retrasarse. Tras la detección de capacidades, la compilación debe seleccionar tipos estándar, la interfaz experimental de una implementación o una plantilla escalar. No expongas un tipo de vector privado del compilador en una API pública. Cada ruta debe ejecutar las mismas pruebas de corrección.

6. Demostrar el rendimiento y los límites

Fija N, la distribución de datos, las opciones del compilador y la cantidad de hilos en benchmarks que comparen la versión escalar, SIMD y diferentes anchos. Registra throughput, latencia, fallos de caché, la proporción de instrucciones vectoriales y el ancho de banda de memoria; prueba N=1,000, entrada vacía, una dirección desalineada y datos con NaN por separado. Si el ancho de banda de memoria o la sobrecarga de llamadas es el cuello de botella, SIMD podría no ayudar y la implementación simple debería mantenerse.

Respuesta de ejemplo sólida

“Trato a std::simd como una abstracción paralela a nivel de datos portable y no asumo un conteo de carriles o una instrucción fija. El bucle principal procesa lotes completos de V::size() y utiliza element_aligned_tag para arreglos que puedan no estar alineados; la cola utiliza un bucle escalar o una máscara que restringe lecturas y escrituras. Defino primero el aliasing, la propagación de NaN y los requisitos de error, y luego selecciono C++26, una implementación experimental o una plantilla escalar con feature tests. Pruebo N=1,000, entrada vacía, direcciones desalineadas y NaN, y comparo el throughput, el comportamiento de caché y el ancho de banda con contadores de hardware. Si la memoria limita el bucle, no fuerzo la vectorización”.

Errores comunes

  • Tratar a std::simd como un registro de ancho fijo → el código depende de una sola CPU → usar de manera deliberada el ancho seleccionado por la implementación o fijado explícitamente.
  • Cargar un vector completo para la cola → acceso fuera de límites → usar una máscara o un bucle de cola escalar.
  • Asumir alineación → comportamiento indefinido o cargas más lentas → hacer coincidir la etiqueta de carga con la garantía real.
  • Comparar únicamente valores promedio → las diferencias de NaN, infinito y redondeo desaparecen → definir contratos de valores especiales y de error.
  • Declarar victoria tras ver instrucciones vectoriales → el ancho de banda de memoria puede dominar → demostrarlo con benchmarks fijos y contadores.

Preguntas de seguimiento y respuestas

¿Cómo eliges entre ancho fijo y nativo?

El ancho nativo permite que la implementación elija un ancho de registro para el hardware de destino y usualmente se adapta al throughput. El ancho fijo es adecuado para un layout estable, ABI o comportamiento reproducible multiplataforma. Revisa la ABI, el layout de datos y el benchmark antes de elegir; el ancho fijo no es en sí mismo una garantía de rendimiento.

¿Por qué no usar siempre una máscara para la cola?

Una máscara mantiene una única forma de bucle, pero puede añadir sobrecarga de construcción, carga y almacenamiento. Una cola corta es más fácil de auditar con código escalar; compara las colas medidas con máscara y escalares cuando la proporción de la cola sea relevante.

¿Cómo demuestras que el compilador no scalarizó el bucle?

Inspecciona el ensamblador generado o los informes de optimización y emparéjalos con contadores de hardware para instrucciones vectoriales, throughput y comportamiento de caché. La inspección del código fuente o una única muestra de tiempo de reloj es insuficiente; mantén constantes el compilador, los flags de destino y el tamaño de datos.

¿Qué pasa si el encabezado simd no está disponible en el compilador de producción?

Utiliza feature tests y una matriz de compilación para seleccionar una implementación soportada manteniendo la corrección escalar. No copies un tipo de vector privado en la interfaz pública. Habilita la ruta estándar tras una actualización de la cadena de herramientas y reutiliza las mismas pruebas numéricas y de límites.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta