Por qué log no es monótona en PHP y Lua: el caso base que rompe la intuición

Fuentes: log is non-monotonous in PHP and Lua, purplesyringa.moe

Una falla silenciosa en la intuición matemática del cómputo

En el mundo del software, una función tan aparentemente inocente como el logaritmo esconde trampas que rompen las expectativas más básicas de los programadores. Un análisis publicado por el desarrollador conocido como purplesyringa revela que PHP y Lua —dos lenguajes ampliamente utilizados en producción— pueden devolver resultados contraintuitivos al calcular logaritmos con bases casi idénticas, mientras que Rust y C# no presentan este problema.

El caso de estudio es sencillo en apariencia. Dado un valor x = 2.93, y dos bases a = 10 + 2^-49 y b = 10, es matemáticamente cierto que a > b, y por tanto log(x, a) < log(x, b). Sin embargo, al ejecutar este código en PHP, la comparación devuelve un resultado que contradice la intuición: log(x, a) resulta mayor que log(x, b). Lua reproduce el mismo fallo, pero Rust y C# mantienen el orden esperado. Todo, según el autor, en la misma máquina y sistema operativo.

El autor subraya que no se trata del habitual "error de coma flotante" que todo programador aprende a tolerar. El ejemplo fue diseñado específicamente para exponer un fenómeno distinto, ligado a las decisiones de implementación de cada lenguaje.

La raíz del problema está en libm, la biblioteca que provee las funciones trascendentales en la mayoría de los lenguajes. Esta biblioteca expone funciones optimizadas para bases concretas: logaritmo natural, base 10 y base 2. Sin embargo, no existe una función genérica para una base arbitraria, por lo que los lenguajes que ofrecen log(x, base) calculan el resultado dividiendo el logaritmo natural del argumento entre el logaritmo natural de la base. Esta fórmula, aunque matemáticamente correcta, introduce un doble redondeo que la hace ligeramente imprecisa.

Hasta ahí, nada nuevo. El verdadero problema surge cuando el lenguaje decide optimizar casos especiales: cuando la base es exactamente 10 (o 2), PHP y Lua no recurren a la fórmula genérica, sino que invocan directamente la función especializada de libm para esa base, más precisa y rápida. El resto de las veces, aplican la división de logaritmos naturales.

El artículo demuestra que esto genera una discontinuidad en el límite entre ambos métodos. Cuando la base se acerca a 10 —por ejemplo, 10 + 2^-49, una diferencia minúscula pero suficiente para activar la ruta genérica— el resultado se desvía en sentido opuesto al que se obtendría usando la ruta optimizada. Es lo que el autor describe como un "agujero en forma de log10" dentro del patrón de error de precisión: un hueco donde la continuidad del resultado se rompe porque cambian las reglas de cálculo.

Según purplesyringa, el problema no es un error en el sentido estricto del término, sino un descuido de diseño con consecuencias reales. "IEEE-754 es increíblemente robusto, pero las decisiones de implementación irreflexivas aquí y allá lo contaminan hasta el punto de que la gente atribuye cualquier bug de coma flotante a la imprecisión intrínseca", señala el autor, que critica cómo estos casos refuerzan el desdén general hacia la aritmética de punto flotante.

El artículo también documenta una inconsistencia añadida: PHP y C# tienen un caso especial para log(x, 1) que devuelve NaN sin importar x, mientras que Lua y Python suelen devolver 0, que sería el resultado matemáticamente correcto. Además, la firma de la función log en PHP indica que el valor por defecto de la base es M_E, la constante de Euler, pero en la práctica, al no especificar base, el comportamiento puede diferir del que se obtiene al invocarla explícitamente como log(x, M_E), porque M_PI o sin(M_PI) no se evalúan de forma idéntica en la tabla de constantes.

Como posibles soluciones, el autor sugiere que Lua debería incorporar math.log10 como función independiente y eliminar la ramificación especial dentro de math.log, separando así los caminos de cálculo. Irónicamente, según el análisis, Lua 5.2 fue en la dirección opuesta. En el caso de PHP, se recomienda revisar la documentación y considerar la misma separación entre funciones dedicadas y la genérica.

Lo que este caso pone en evidencia es que las optimizaciones de rendimiento, aunque bien intencionadas, pueden introducir discontinuidades matemáticas que comprometen propiedades básicas de monotonía y consistencia. Para los desarrolladores que dependen de cálculos numéricos precisos —en finanzas, simulación científica o aprendizaje automático—, la lección es clara: no basta con confiar en que las funciones matemáticas de un lenguaje "harán lo correcto". Es necesario entender cómo están implementadas, y en qué casos los atajos de optimización pueden jugar en contra.

El análisis de purplesyringa se suma a una creciente literatura sobre los peligros ocultos de la aritmética de coma flotante, recordatorio oportuno de que la matemática del software sigue siendo, en muchos rincones, territorio donde la intuición puede traicionar incluso a los lenguajes más maduros.