Rehenes COBOL y Matemáticas. Parte 1

Seamos realistas: a nadie le gustan los números fraccionarios, incluso las computadoras.

Cuando se trata del lenguaje de programación COBOL, la primera pregunta que surge en la cabeza de todos siempre se ve así: "¿Por qué la humanidad todavía usa este lenguaje en muchas áreas vitales?" Los bancos todavía usan COBOL. Alrededor del 7% del PIB de los Estados Unidos depende de COBOL en el procesamiento de pagos de CMS . El Servicio de Impuestos Internos (IRS) de los Estados Unidos, como todos saben, todavía usa COBOL. Este lenguaje también se usa en la aviación ( desde aquí aprendí algo interesante sobre este tema: el número de reserva en los boletos aéreos solía ser un puntero habitual). Se puede decir que muchas organizaciones muy serias, ya sea un sector privado o público, todavía están usando COBOL.



La segunda parte

El autor del material, cuya primera parte de la traducción que publicamos hoy, encontrará una respuesta a la pregunta de por qué COBOL, el lenguaje que apareció en 1959, todavía está tan extendido.

¿Por qué COBOL sigue vivo?


La respuesta tradicional a esta pregunta es profundamente cínica. Las organizaciones son pereza, incompetencia y estupidez. Persiguen lo barato y no están dispuestos a invertir en reescribir sus sistemas de software en algo moderno. En general, se puede suponer que la razón por la cual el trabajo de un número tan significativo de organizaciones depende de COBOL es una combinación de inercia y miopía. Y en esto, por supuesto, hay algo de verdad. Reescribir grandes cantidades de código confuso es una tarea enorme. Es caro Esto es complicado Y si el software existente parece funcionar bien, la organización no tendrá una motivación particularmente fuerte para invertir en un proyecto para actualizar este software.

Todo esto es así. Pero cuando trabajaba en el IRS, los veteranos de COBOL hablaron sobre cómo trataron de reescribir el código en Java y resultó que Java no podía realizar los cálculos correctamente.

Me sonó extremadamente extraño. Es tan extraño que inmediatamente pensé en el alarmista: "¡Señor, eso significa que el IRS ha estado redondeando los pagos de impuestos a todos por 50 años!" Simplemente no podía creer que COBOL sea capaz de sortear Java en términos de los cálculos matemáticos que necesita el IRS. Al final, no lanzaron personas al espacio.

Uno de los efectos secundarios interesantes de aprender COBOL en el verano es que comencé a entender lo siguiente. El punto no es que Java no pueda realizar correctamente cálculos matemáticos. El punto es exactamente cómo Java hace los cálculos correctos. Y cuando comprende cómo se realizan los cálculos en Java y cómo se hace lo mismo en COBOL, comienza a comprender por qué a muchas organizaciones les resulta tan difícil deshacerse de su legado informático.

¿Qué "i" debe estar punteado?


Voy a alejarme un poco de la historia de COBOL y hablar sobre cómo las computadoras almacenaron información antes de que la representación binaria de datos se convirtiera en el estándar de facto (pero el material sobre cómo usar la interfaz z / OS; esto es algo especial). Creo que al considerar nuestro problema, será útil desviarse del tema principal en esta dirección. En el material anterior, hablé sobre varias formas de usar interruptores binarios para almacenar números en sistemas binarios, ternarios, decimales, para almacenar números negativos, y así sucesivamente. Lo único a lo que no le presté suficiente atención fue cómo se almacenan los números decimales.

Si diseñó su propia computadora binaria, entonces podría comenzar decidiendo que usará el sistema de números binarios. Los bits a la izquierda del punto representan enteros - 1, 2, 4, 8. Y los bits a la derecha - números fraccionarios - 1/2, 1/4, 1/8 ...


2,75 en representación binaria

El problema aquí es entender cómo almacenar el punto decimal en sí mismo (de hecho, debería decir "punto binario", porque, después de todo, estamos hablando de números binarios). Esto no es una especie de "alquimia informática", por lo que puede adivinar de qué estoy hablando de números de punto flotante y números de punto fijo. En los números de coma flotante, un punto binario se puede colocar en cualquier lugar (es decir, puede "flotar"). La posición del punto se almacena como un exponente. La capacidad de mover un punto hace posible almacenar un rango de números más amplio que el que está disponible en ausencia de tal oportunidad. El punto decimal se puede mover a la parte posterior del número y seleccionar todos los bits para almacenar valores enteros, que representan números muy grandes. El punto puede desplazarse al frente del número y expresar valores muy pequeños. Pero esta libertad tiene un precio de precisión. Echemos otro vistazo a la representación binaria de 2.75 del ejemplo anterior. Una transición de cuatro a ocho es mucho más que una transición de un cuarto a un octavo. Puede ser más fácil para nosotros imaginar esto si reescribimos el ejemplo como se muestra a continuación.


Elegí la distancia entre los números a simple vista, solo para demostrar mi idea.

La diferencia entre los números es fácil de calcular por su cuenta. Por ejemplo, la distancia entre 1/16 y 1/32 es 0.03125, pero la distancia entre 1/2 y 1/4 ya es 0.25.

¿Por qué es esto importante? En el caso de una representación binaria de enteros, esto no importa: la distancia entre los números adyacentes de un registro binario se puede compensar fácilmente llenándolos con las combinaciones apropiadas de bits y sin perder precisión. Pero en el caso de la representación de números fraccionarios, no es tan simple. Si intenta "llenar" los "agujeros" entre números adyacentes, algo puede "caer" (y en realidad caer) en estos agujeros. Esto lleva al hecho de que en formato binario no es posible obtener representaciones exactas de números fraccionarios.

Esto se ilustra con el ejemplo clásico del número 0.1 (una décima). ¿Cómo representar este número en formato binario? 2 -1 es 1/2, o 0.5. Esto es demasiado 1/16 es 0.0635. Esto es muy poco. 1/16 + 1/32 ya está más cerca (0.09375), pero 1/16 + 1/32 + 1/64 ya es más de lo que necesitamos (0.109375).

Si cree que este razonamiento puede continuar indefinidamente, entonces tiene razón, tal como está .

Aquí puede decirse a sí mismo: “¿Por qué no ahorramos 0.1 de la misma manera que almacenamos el número 1? Podemos guardar el número 1 sin ningún problema, así que simplemente eliminemos el punto decimal y almacenemos cualquier número de la misma manera que almacenamos enteros ".

Esta es una excelente solución a este problema, excepto que requiere fijar el punto binario / decimal en una ubicación predeterminada. De lo contrario, los números 10.00001 y 100000.1 se verán exactamente iguales. Pero si el punto es fijo de modo que, digamos, se asignan 2 dígitos a la parte fraccionaria del número, entonces podemos redondear 10.00001 a 10.00, y 100000.1 dará vuelta 100000.10.

Acabamos de "inventar" números de punto fijo.

Con la representación de diferentes valores usando números de punto fijo, simplemente lo descubrimos. Es facil de hacer. ¿Es posible, utilizando números de punto fijo, facilitar la solución de otros problemas? Recordemos aquí sobre nuestros buenos amigos, sobre números decimales binarios (Binary Coded Decimal, BCD). Por cierto, para hacerle saber, estos números se usan en la mayoría de las calculadoras científicas y gráficas. De estos dispositivos, que es bastante claro, esperan los resultados correctos de los cálculos.


Calculadora TI-84 Plus

Relación de recurrencia de Muller y Python


Los números de punto fijo se consideran más precisos debido al hecho de que los "agujeros" entre los números son constantes, y porque el redondeo ocurre solo cuando necesita imaginar un número para el que simplemente no hay suficiente espacio. Pero cuando usamos números de coma flotante, podemos representar números muy grandes y muy pequeños usando la misma cantidad de memoria. Es cierto que con su ayuda es imposible representar todos los números en el rango accesible con precisión y nos vemos obligados a recurrir al redondeo para llenar los "agujeros".

COBOL se creó como un idioma en el que, por defecto, se utilizan números de punto fijo. ¿Pero esto significa que COBOL es mejor que los lenguajes modernos para realizar cálculos matemáticos? Si nos damos cuenta de un problema como el resultado del cálculo del valor 0.1 + 0.2, puede parecer que la pregunta anterior debería responderse "sí". Pero será aburrido. Así que sigamos adelante.

Vamos a experimentar con COBOL utilizando la llamada relación de recurrencia de Muller. Jean-Michel Muller es un científico francés que pudo haber realizado un importante descubrimiento científico en el campo de la tecnología de la información. Encontró una manera de romper el funcionamiento correcto de las computadoras usando las matemáticas. Estoy seguro de que diría que estudia los problemas de fiabilidad y precisión, pero no y no otra vez: crea problemas matemáticos que "rompen" las computadoras. Una de estas tareas es su fórmula de recurrencia. Se ve así:


Este ejemplo está tomado de aquí.

La fórmula no parece aterradora en absoluto. Derecho? Esta tarea es adecuada para nuestros propósitos por las siguientes razones:

  • Aquí solo se usan reglas simples de matemáticas: no hay fórmulas complicadas ni ideas profundas.
  • Comenzamos con un número que tiene dos dígitos después del punto decimal. Como resultado, es fácil imaginar que estamos trabajando con valores que representan ciertas cantidades de dinero.
  • El error que resulta de los cálculos no es un pequeño error de redondeo. Esta es una desviación del resultado correcto en órdenes de magnitud completas.

Aquí hay un pequeño script de Python que calcula los resultados de la relación de recurrencia de Mueller usando números de punto flotante y de punto fijo:

from decimal import Decimal def rec(y, z):  return 108 - ((815-1500/z)/y)  def floatpt(N):  x = [4, 4.25]  for i in range(2, N+1):   x.append(rec(x[i-1], x[i-2]))  return x  def fixedpt(N):  x = [Decimal(4), Decimal(17)/Decimal(4)]  for i in range(2, N+1):   x.append(rec(x[i-1], x[i-2]))  return x N = 20 flt = floatpt(N) fxd = fixedpt(N) for i in range(N):  print str(i) + ' | '+str(flt[i])+' | '+str(fxd[i]) 

Aquí está el resultado de este script:

 i | floating pt  | fixed pt -- | -------------- | --------------------------- 0 | 4       | 4 1 | 4.25      | 4.25 2 | 4.47058823529 | 4.4705882352941176470588235 3 | 4.64473684211 | 4.6447368421052631578947362 4 | 4.77053824363 | 4.7705382436260623229461618 5 | 4.85570071257 | 4.8557007125890736342039857 6 | 4.91084749866 | 4.9108474990827932004342938 7 | 4.94553739553 | 4.9455374041239167246519529 8 | 4.96696240804 | 4.9669625817627005962571288 9 | 4.98004220429 | 4.9800457013556311118526582 10 | 4.9879092328  | 4.9879794484783912679439415 11 | 4.99136264131 | 4.9927702880620482067468253 12 | 4.96745509555 | 4.9956558915062356478184985 13 | 4.42969049831 | 4.9973912683733697540253088 14 | -7.81723657846 | 4.9984339437852482376781601 15 | 168.939167671 | 4.9990600687785413938424188 16 | 102.039963152 | 4.9994358732880376990501184 17 | 100.099947516 | 4.9996602467866575821700634 18 | 100.004992041 | 4.9997713526716167817979714 19 | 100.000249579 | 4.9993671517118171375788238 

Hasta la iteración 12, el error de redondeo parece más o menos insignificante, pero luego comienza el verdadero infierno. Los cálculos de punto flotante convergen a un número que es veinte veces mayor que el resultado de los cálculos de punto fijo.

Quizás piense que es poco probable que alguien realice cálculos recursivos a gran escala. Pero esto es precisamente lo que causó el desastre de 1991, que provocó la muerte de 28 personas, cuando el sistema de control de misiles Patriot calculó incorrectamente el tiempo. Resultó que los cálculos de coma flotante causaron mucho daño accidentalmente. Aquí hay algunas cosas excelentes que tal vez la informática de alto rendimiento es solo una forma más rápida de obtener las respuestas incorrectas. Lea este trabajo si desea obtener más información sobre el problema discutido aquí y vea más ejemplos.

El problema es que la cantidad de RAM que tienen las computadoras no es infinita. Por lo tanto, es imposible almacenar un número infinito de posiciones decimales (o binarias). Los cálculos de punto fijo pueden ser más precisos que los cálculos de punto flotante si existe la confianza de que es menos probable que se necesiten más números después del punto que el formato utilizado. Si el número no cabe en este formato, se redondeará. Cabe señalar que ni los cálculos de punto fijo ni los de punto flotante están protegidos del problema que demuestra la relación de recurrencia de Mueller. Tanto eso como otros como resultado dan resultados incorrectos. La pregunta es cuándo sucede esto. Si aumenta el número de iteraciones en un script de Python, por ejemplo, de 20 a 22, el número final obtenido en los cálculos con un punto fijo será 0.728107. 23 iteraciones? -501.7081261. 24? 105.8598187.

En diferentes idiomas, este problema se manifiesta de diferentes maneras. Algunos, como COBOL, le permiten trabajar con números cuyos parámetros están bien ajustados. Y en Python, por ejemplo, hay valores predeterminados que se pueden configurar si la computadora tiene suficiente memoria. Si agregamos la línea getcontext().prec = 60 a nuestro programa, diciéndole al módulo decimal de Python que usaría 60 posiciones después del período, y no 28, como se hace por defecto, el programa podrá realizar 40 iteraciones de la relación de recurrencia sin errores Mueller

Continuará ...

Estimados lectores! ¿Ha encontrado serios problemas derivados de la naturaleza de los cálculos de coma flotante?

Source: https://habr.com/ru/post/467251/


All Articles