C++20 introduce
, una función que dados tres números a, b y t, calcula la interpolación lineal entre a y b en t. Incluye overloads para todas las combinaciones de tipos numéricos en el lenguaje y hace lo que tiene que hacer. Es más, lo hace muy bien. Lo hace tan bien que para todo t entre 0 y 1 el resultado es
correcto.
Parte 1:
puede ser innecesariamente lento
Si implementamos esta función, lo más probable es que escribamos algo así:
float lerp(float a, float b, float t)
{
return a + (b – a) * t;
}
Este código parece correcto. Bien, ¿lo es? Lo es en todos los casos excepto cuando el valor intermedio (b - a) es menor de lo que un float puede representar. En ese caso, el cálculo es incorrecto.
en cambio calcula correctamente este caso y devuelve el resultado correcto incluso ante situaciones de valores intermedios no representables. Sabiendo esto, uno pensaría que
es perfecto. No sólo nos viene ya hecho, sino que encima es más correcto que la versión que escribiríamos nosotros. ¿Dónde está el problema entonces?
Bien, el problema es simple. Esta garantía tiene un coste. Para poder asegurarse de que no va a caer en una situación en la que uno de los valores intermedios no es representable,
analiza sus parámetros y decide qué fórmula va a usar para calcular la interpolación lineal de forma que siempre elija una correcta. Esto hace de
significativamente más lento que nuestra versión. Esto se puede ver muy bien en el assembly.
nuestro_lerp(float, float, float):
subss xmm1, xmm0
mulss xmm1, xmm2
addss xmm0, xmm1
ret
std_lerp(float, float, float):
xorps xmm3, xmm3
ucomiss xmm3, xmm0
jb .LBB0_2
ucomiss xmm1, xmm3
jae .LBB0_4
.LBB0_2:
ucomiss xmm0, xmm3
jb .LBB0_5
xorps xmm3, xmm3
ucomiss xmm3, xmm1
jb .LBB0_5
.LBB0_4:
mulss xmm1, xmm2
movss xmm3, dword ptr [rip + .LCPI0_0] # xmm3 = mem[0],zero,zero,zero
subss xmm3, xmm2
mulss xmm3, xmm0
addss xmm1, xmm3
movaps xmm0, xmm1
ret
.LBB0_5:
ucomiss xmm2, dword ptr [rip + .LCPI0_0]
jne .LBB0_6
jp .LBB0_6
movaps xmm0, xmm1
ret
.LBB0_6:
movaps xmm3, xmm1
subss xmm3, xmm0
mulss xmm3, xmm2
ucomiss xmm2, dword ptr [rip + .LCPI0_0]
setbe al
ucomiss xmm1, xmm0
addss xmm3, xmm0
seta cl
xor cl, al
jne .LBB0_7
minss xmm3, xmm1
movaps xmm1, xmm3
movaps xmm0, xmm1
ret
.LBB0_7:
maxss xmm3, xmm1
movaps xmm1, xmm3
movaps xmm0, xmm1
ret
Mientras que nuestro lerp sólo hace una resta, una multiplicación y una suma, podemos ver como
tiene una serie de saltos condicionales y cuatro ramas en las que puede terminar. Tras algunas mediciones, dependiendo de la capacidad de la CPU para predecir las condiciones,
es entre un 10% y un 40% más caro. Este incremento del coste debería hacernos preguntarnos cómo de útil es en realidad esta garantía adicional que nos brinda, y basar nuestra decisión en torno a esto.
Una de las situaciones más comunes en las que se usa la interpolación lineal es en animaciones de sistemas de partículas y esqueletos. En estos, lerp es la operación más común, y se calcula potencialmente varios miles (o millones) de veces por fotograma. También, por lo general se opera con valores conocidos de antemano, decididos a mano por un diseñador, un animador o un artista de efectos especiales, de forma que se sabe que nunca se va a dar el caso de estos valores tan grandes que no son representables por el tipo de dato que se está usando. En estos casos, la decisión de usar std::lerp es errónea, ya que se está introduciendo un coste adicional en el código sin que esto reporte ningún beneficio a cambio.
No estoy diciendo que
esté mal ni que no haya que usarlo. Si eso, está mal especificado, o mal entendido. Tal vez mal nombrado. La cualidad que define a std::lerp no es la de calcular la interpolación lineal, sino la de hacerlo correctamente en todas las situaciones, y debería usarse únicamente cuando existe la posibilidad de que se de un caso en el que la implementación de lerp mostrada arriba se rompa. En los casos en los que se sabe de antemano que este tipo de errores no van a darse, usar
es una decisión equivocada.
Parte 2:
está incorrectamente abstraído
La interpolación lineal es una operación definida sobre un conjunto V que define la suma y el producto escalar. Este conjunto es, evidentemente, un espacio vectorial. Sin embargo,
está definido únicamente para números. Esto nos impide interpolar linealmente vectores, polinomios, cuaterniones o colores RGB, todos ellos candidatos muy frecuentes a la interpolación lineal en aplicaciones reales. Una definición correcta de lerp en C++, asumiendo los conceptos VectorSpace y Real, sería
template <VectorSpace V, Real R>
V lerp(V a, V b, R t)
{
return a + (b – a) * t;
}
Sospecho que la implementación actual se da debido al hecho de tener que llevar a cabo estas comprobaciones de los casos especiales, que no sería posible sobre un espacio vectorial arbitrario.