- La respuesta de Java
humanReadableByteCount, escrita en 2010, fue identificada en un estudio de 2018 como el fragmento de código de Stack Overflow más copiado, pero producía resultados incorrectos en los valores límite del formateo de tamaños en bytes
- Este código aprovechaba que prefijos como
kB, MB y GB son potencias de 1000 o 1024, y elegía la unidad mediante cálculo logarítmico en lugar de un bucle
- El bug principal era un problema de umbral de redondeo por el que
999,999 bytes se mostraba como "1000.0 kB" en modo SI, cuando según la especificación, si el rango numérico debía ser de 1 a 999.9, lo correcto era "1.0 MB"
- En valores más grandes también se sumaba la limitación de precisión de punto flotante de
double, por lo que la entrada 999,949,999,999,999,999 devolvía 1000.0 PB; para corregirlo hicieron falta cálculo de umbrales, reducción de escala, ajuste del patrón de bits y strictfp
- El código final maneja negativos y hasta
Long.MIN_VALUE, pero perdió la simplicidad original; al copiar código de Stack Overflow también hacen falta pruebas de casos límite y atribución de la fuente
La simplificación que buscaba la respuesta de 2010
- El problema era dar formato a una cantidad de bytes como una cadena fácil de leer
- Ejemplo: mostrar
123,456,789 bytes como "123.5 MB"
- La especificación implícita era que la parte numérica del resultado estuviera entre 1 y 999.9, con el sufijo de tamaño adecuado
- La respuesta previa usaba un enfoque basado en bucles: recorría
EB, PB, TB, GB, MB, kB, B desde la unidad más grande y elegía la primera menor que la cantidad de bytes
- La nueva respuesta usaba
Math.log y Math.pow para reducir bucles y ramas
- En modo SI, la unidad es
1000
- En notación binaria, la unidad es
1024
- El valor
exp = log(bytes) / log(unit) se convertía a entero y se usaba como índice del prefijo
- Los prefijos eran
"kMGTPE" en SI y "KMGTPE" en binario, agregando "i" en el caso binario
El alcance de la copia y el episodio de OpenJDK
- El artículo de Sebastian Baltes Usage and Attribution of Stack Overflow Code Snippets in GitHub Projects analiza cómo se usan los fragmentos de código de Stack Overflow en proyectos de GitHub y cómo se atribuye su origen
- El método consistía en extraer fragmentos de código del volcado de datos de Stack Overflow y compararlos con código de repositorios públicos de GitHub
- La pregunta central era si se estaba cumpliendo la atribución exigida por la licencia CC BY-SA 3.0 de Stack Overflow
- El resultado fue que la mayoría de los usuarios no incluía la atribución adecuada
- La respuesta con ID 3758880 aparecía en la primera posición de la tabla del artículo y en ese momento tenía cientos de miles de vistas y más de 1,000 votos positivos
- Si buscas
humanReadableByteCount en GitHub, aparecen miles de casos de uso, y en un repositorio local puedes comprobarlo con este comando
git grep humanReadableByteCount
- También se encontró una coincidencia en el repositorio de OpenJDK
- Ese código no incluía atribución, y la licencia de OpenJDK no era compatible con CC BY-SA 3.0
- Sebastian Baltes preguntó en la lista de correo de desarrollo de OpenJDK si el código había sido copiado de Stack Overflow hacia OpenJDK o al revés
- El autor de la respuesta no trabajaba todavía en Oracle cuando se integró ese commit y no contribuyó a ese parche
- Después se registró un issue y el código fue eliminado
El primer bug: el valor límite con muchos 999
- Los problemas que parecían sospechosos a primera vista no eran la causa real
- El valor máximo de
long es 2^63 - 1, aproximadamente 9.2 × 10^18, así que no llega a pasar de las unidades posteriores a EB
- Cuando
bytes < unit, el primer if lo maneja, así que exp no llega a ser 0 ni falla charAt(exp - 1)
- El problema real era el umbral de redondeo
- La entrada
999,999 bytes se convertía en "1000.0 kB" en modo SI
- Si la especificación exige que la parte numérica esté entre 1 y 999.9, el resultado correcto es
"1.0 MB"
- En el momento de escribirlo, las 22 respuestas publicadas, incluyendo las que usaban Apache Commons y bibliotecas de Android, tenían este bug o alguna variante
- La clave de la solución era el umbral que decide cuándo subir
exp a la siguiente unidad
- El punto de cambio de
k a M es 999,950, donde el valor ya está más cerca de 1 MB que de 999.9 k
- El cambio de
M a G ocurre en 999,950,000
- En modo binario, el umbral no es un entero, así que hace falta
ceil
if (bytes >= Math.ceil(Math.pow(unit, exp) * (unit - 0.05)))
exp++;
El segundo bug: el límite de precisión de double
- Incluso con esa corrección, la entrada
999,949,999,999,999,999 seguía mostrando 1000.0 PB, cuando el resultado correcto era 999.9 PB
- La causa no era la fórmula matemática en sí, sino el límite de precisión de
double
- En la representación IEEE 754, los valores de punto flotante cercanos a 0 son densos, pero los valores grandes están mucho más espaciados
- Con
double muy grandes, incluso restar Long.MAX_VALUE puede no cambiar el valor
double a = Double.MAX_VALUE;
double b = a - Long.MAX_VALUE;
System.err.println(a == b); // prints true
- El cálculo problemático aparecía en dos lugares
- La división realizada en el argumento de
String.format
- El cálculo del umbral para decidir si había que aumentar
exp
- El primer problema se resolvió reduciendo el valor intermedio de
bytes a un rango con mejor precisión y ajustando exp
- La idea es que el resultado final de todos modos se redondea, así que se pueden descartar dígitos menos significativos
if (exp > 4) {
bytes /= unit;
exp--;
}
- En el segundo problema, los bits bajos sí importaban
999,949,99…9 y 999,950,00…0 debían clasificarse con exponentes distintos
- Había 12 umbrales posibles entre SI y binario, y solo uno producía un resultado incorrecto
- Ese resultado erróneo se identificó y corrigió mediante un patrón de bits que terminaba en
D00
- Como la corrección dependía del patrón de bits de un resultado específico de punto flotante, se añadió
strictfp
Entradas negativas y el código final
- Como Java no tiene
long sin signo, también se añadió manejo para cantidades negativas de bytes
- Antes, una entrada de
-10,000 se mostraba como -10000 B
- Se introdujo
absBytes para que los cálculos relacionados con exp se hicieran sobre el valor absoluto
Long.MIN_VALUE requería un tratamiento especial
- Porque
-Long.MIN_VALUE == Long.MIN_VALUE
- Por eso, si
bytes == Long.MIN_VALUE, se usa Long.MAX_VALUE; en los demás casos se usa Math.abs(bytes)
- La versión final incluye
strictfp, corrección de umbrales, manejo de Long.MIN_VALUE y reducción de escala en exponentes grandes
- El código que buscaba evitar bucles y ramas excesivas terminó siendo más difícil de leer que la versión original después de pulir todos los casos límite
- Para un código moderno con calidad de producción, puede consultarse este artículo aparte: Formatting byte size to human readable format
La lección que queda para el trabajo real
- Un fragmento de código de Stack Overflow puede tener bugs, incluso con miles de votos positivos
- El código copiado necesita, sobre todo, pruebas de casos límite
- La aritmética de punto flotante es difícil de manejar en valores límite y números grandes
- Al copiar código, hace falta una atribución adecuada, o eso puede convertirse en un problema real
1 comentarios
Opiniones en Hacker News
Es interesante que todas las respuestas que usan valores hardcodeados y sentencias
if(owhile) hacen como máximo 5 comparaciones.Si las unidades solo llegan hasta B, KiB, MiB, GiB, TiB y EiB, también se puede resolver con un máximo de 3 sentencias
if. Si verificas si es GiB o más, ya sabes que no es B/KiB/MiB, así que gana la búsqueda binaria.Incluso si se extiende hasta ZiB y YiB, bastan como máximo 3 comparaciones, mientras que el enfoque hardcodeado llega hasta 7. Si yo lo escribiera, no usaría
log/pow/punto flotante porque la probabilidad de equivocarse es demasiado alta; hardcodearía sentenciasif, pero usando búsqueda binaria.Con este tipo de código se está haciendo mucho trabajo para usar código más lento, más complejo y más difícil de probar y revisar.
(2019) Discusiones anteriores:
https://news.ycombinator.com/item?id=21693431
https://news.ycombinator.com/item?id=21698619
https://news.ycombinator.com/item?id=27533684
The most copied StackOverflow snippet of all time is flawed (2019) - https://news.ycombinator.com/item?id=27533684 - junio de 2021, 334 comentarios
The most copied StackOverflow snippet of all time is flawed - https://news.ycombinator.com/item?id=21698619 - diciembre de 2019, 88 comentarios
The most copied StackOverflow snippet of all time is flawed - https://news.ycombinator.com/item?id=21693431 - diciembre de 2019, 3 comentarios
No lo entiendo. Si hay 7 sufijos, puedes elegir el correcto con búsqueda binaria, y bastan 3 comparaciones. O, si lo haces de forma simple, son solo 6 comparaciones.
No veo por qué usar
log()dos veces,pow()una vez yceil()sería mejor que el enfoque simple. El bug descrito aquí es un ejemplo perfecto de lo que pasa por querer pasarse de listo.Aun así, como toma en cuenta el bug de redondeo, es un poco mejor que el primer ejemplo de código del artículo original.
Además, las 6 comparaciones solo ocurren en el valor máximo, y en el uso real parece poco probable. Si la mayoría de los valores están en el rango de B o KB, el enfoque lineal podría ser mejor.
Es autopromoción descarada, pero si en vez de copiar de S/O quieres formatear tamaños de forma rápida y precisa en un formato legible para humanos, también puedes usar nuestra biblioteca open source PrettySize. Hay una versión para Rust [0] y otra para .NET [1], y también hace que las operaciones lógicas type-safe sobre tamaños de archivo sean seguras y fáciles.
El snippet de S/O tiene 4 líneas, pero estas bibliotecas son mucho más completas e incluyen pruebas, opciones de formato de salida, conversiones de tamaño, etc.
[0]: https://github.com/neosmart/prettysize-rs
[1]: https://github.com/neosmart/PrettySize.net
Es pura curiosidad, pero ¿de verdad hay bastantes desarrolladores que simplemente copian código no confiable de Stack Overflow y lo pegan en sus aplicaciones?
Es famosa la suposición de que la gente simplemente copia de Stack Overflow, pero hasta ver a alguien hacerlo de verdad, pensaba que era casi un chiste. Yo también uso Stack Overflow como punto de partida cuando resuelvo problemas en áreas que no conozco bien, pero nunca he copiado el código tal cual.
Normalmente un fragmento de código no hace exactamente solo lo que necesito, así que tengo que revisar la API y construir mi propia solución con base en el enfoque explicado. Especialmente en Python, Stack Overflow muchas veces me ha orientado hacia APIs de nicho útiles.
Literalmente era Google → clic en el primer enlace de Stack Overflow que aparecía → copiar/pegar el primer bloque de código que veía, y a veces ni siquiera era el mismo lenguaje. Durante la programación en pareja había que quitarle físicamente los dispositivos de entrada. Si le decías que estaba mal, antes de que terminaras de hablar ya estaba pegando el segundo fragmento de código de la página, y era extrañamente rápido.
Es un caso extremo, pero hay muchos desarrolladores con la mentalidad de “necesito código; Stack Overflow tiene código; ¡resuelto!”, sin pensar en absoluto si es la solución adecuada.
De todos modos siempre usamos código de librerías hecho por desconocidos para las partes de plomería que no nos importan mucho. Si quiero profundizar y entenderlo, probablemente lo escriba yo mismo, pero si quiero que esa parte “simplemente funcione” y seguir avanzando con el proyecto, se convierte en desarrollo guiado por errores del compilador.
También cambio los nombres de las variables. Muchas veces hay demasiados
foo,barybaz, lo que dificulta la lectura para una persona. Si vuelvo a encontrar el mismo problema, también me resulta más fácil recordar qué hice que si lo hubiera copiado a ciegas.No entiendo por qué usan un logaritmo de punto flotante si lo que necesitan es
log 2.Si no se me escapa nada, la siguiente expresión da exactamente
floor(log2(value))para valores positivos menores que 2^63 bytes, y es mucho más rápida:Long.bitCount( (Long.highestOneBit(value) << 1) - 1) - 1En cuanto vi el fragmento de código, noté la operación
logde punto flotante y una división sobre enteros, así que lo descarté mentalmente de inmediato como código demasiado ingenioso y, por eso mismo, inherentemente propenso a errores.La cadena de conocimiento llega hasta el fondo. Muestra lo difícil que es volver a guardar incluso un pedacito mínimo de conocimiento una vez que se ha sacado.
Con Stack Exchange perdiendo rápidamente a sus colaboradores activos, me pregunto qué haría falta para corregir esas respuestas de pistolero rápido que después resultan estar equivocadas. Y también qué significa para nuestro conocimiento colectivo que estas respuestas “ligeramente incorrectas” queden cada vez más fijadas en los historiales de búsqueda y, con el tiempo, en la historia de los LLM.
Me recuerda al entrenamiento militar básico. Los instructores solían darles a los reclutas, deliberadamente y sin instrucciones, una tarea que nadie sabía hacer, y luego se iban.
Entonces siempre alguien empezaba de la manera incorrecta, y todos los demás lo seguían.
En los pronósticos económicos públicos pasa algo parecido. A quien se equivoca solo mientras los demás aciertan se lo trata con mucha más dureza que a quienes se equivocan junto con todos.
En este tipo de algoritmos, no necesariamente consideraría los errores de punto flotante como un “defecto”. Si el código define una solución lógica y matemáticamente correcta, para mí eso en sí mismo está “bien”.
Resolver los errores de punto flotante es un nivel por encima de eso, y es algo que solo se hace cuando realmente importa. Puedo imaginar un lenguaje de programación futuro perfecto en el que los errores de punto flotante no existan y ni siquiera haya que considerarlos; el 99% de mis algoritmos, en ese sentido, están dirigidos a un lenguaje así.