Recuerdo que cuando yo tenía unos 14 años, mi profesor de matemáticas comentó que aun siendo π un número irracional, a lo largo de la historia muchas personas intentaron aproximarse a este valor de π mediante fracciones de números enteros. Por ejemplo, la fracción 22/7 da un resultado de 3,1428, que es un valor donde coinciden los 2 primeros decimales. Hay muchas curiosidades sobre el número π, por ejemplo, la anterior fracción 22/7 es conocida como el día de Pi, siendo este el 22 de julio. Hay muchísimas más curiosidades, hoy te voy a mostrar a encontrar fracciones de números enteros que se aproximen a este valor de π: ¿Cuál es la fracción de 2, 3, 4 o más dígitos que más se acerca al número π? Podría hacerlo con distintos lenguajes de programación pero hoy voy a hacerlo con el lenguaje que más rápido maneja el procesador: código máquina ensamblado directamente de lenguaje ensamblador.
Echando un vistazo a Wikipedia y a otras búsquedas en algunos apuntes y en la red, las fracciones de números enteros más conocidas son las siguientes:
En mi juventud con 14 años, algunas veces pasaba el rato tomando una serie de decimales del número π y dividiendo por 10x intentaba encontrar la menor fracción posible. Por ejemplo:
Sin embargo, este proceso era muy lento y el número 31415926... al descomponerlo daba casi siempre números primos muy elevados, con lo cual llegaba un momento en el que no puedes continuar. Entonces, y aprovechando el procesador, se me ocurre que podemos ir haciendo las divisiones una a una y observar cuándo el resultado se aproxima a π
Para esto voy a crear una simple rutina en ensamblador, ya que este lenguaje lo ensamblaremos directamente en código máquina y es el más rápido con el que el procesador puede trabajar, además cuando se haga divisiones con varios decimales, se necesitará principalmente velocidad.
Hacer este ejercício que me he propuesto en ensamblador (procesador x86) es muy interesante ya que hay que trabajar con la Unidad de Punto Flotante (FPU) y normalmente un programador que lo haga en un lenguaje de alto nivel, no presta atención a esto ya que es el compilador el que se encarga de "traducirlo".
Sé que este código se puede optimizar mucho más (más rápido) ya que hacer divisiones cuando el divisor es mayor que el dividendo es una pérdida de tiempo, pero no lo voy a modificar (que es muy sencillo) porque quiero mostrar la rutina lo más clara posible. Si alguien está interesado en optimizar el código que lo comente...
0030BFC3 > $ 68 A0C03000 push 30C0A0 0030BFC8 . B9 E7030000 mov ecx,3E7 ; 999 dec 0030BFCD $ 68 6F12033A push 3A03126F 0030BFD2 . D90424 fld dword ptr ss:[esp] 0030BFD5 . 83C4 04 add esp,4 0030BFD8 . D9EB fldpi 0030BFDA . BB E8030000 mov ebx,3E8 ; 1000 dec 0030BFDF > > 4B dec ebx 0030BFE0 > > 53 push ebx 0030BFE1 . DF0424 fild word ptr ss:[esp] 0030BFE4 . 51 push ecx 0030BFE5 . DA3424 fidiv dword ptr ss:[esp] 0030BFE8 . 83C4 04 add esp,4 0030BFEB . D8E1 fsub st,st(1) 0030BFED . D9E1 fabs 0030BFEF . D8D2 fcom st(2) 0030BFF1 . DFE0 fstsw ax 0030BFF3 . 9E sahf 0030BFF4 . 0F86 1C000000 jbe 0030C016 ; pi.encontrado 0030BFFA > > 3E:D91C24 fstp dword ptr ds:[esp] 0030BFFE . 83C4 04 add esp,4 0030C001 .^ E2 DD l o o p d short 0030BFE0 ; pi.bucle2 0030C003 . B9 E7030000 mov ecx,3E7 0030C008 . 83FB 01 cmp ebx,1 0030C00B . 0F86 1D000000 jbe 0030C02E ; pi.fin 0030C011 .^ E9 C9FFFFFF jmp 0030BFDF ; pi.bucle1 0030C016 > > 8B4424 04 mov eax,dword ptr ss:[esp+4] ; ENCONTRADO 0030C01A . 8918 mov dword ptr ds:[eax],ebx 0030C01C . 834424 04 04 add dword ptr ss:[esp+4],4 0030C021 . 8948 04 mov dword ptr ds:[eax+4],ecx 0030C024 . 834424 04 04 add dword ptr ss:[esp+4],4 0030C029 .^ E9 CCFFFFFF jmp 0030BFFA ; pi.busca_siguiente 0030C02E > > 90 nop ; HA LLEGADO AL FINAL
Te pongo el mismo código tal cual lo he programado:
<$pi.4BFC3> ;Inicializa variables ;3A03126F = 0.0005 push $pi.4C0A0 mov ecx, 3E7 ; 999 dec push 3A03126F fld [esp] add esp,4 fldpi mov ebx, 3E8 ; 1000 dec @bucle1: dec ebx @bucle2: push ebx fild [esp] push ecx fidiv [esp] add esp,4 fsub st,st(1) fabs fcom st(2) fstsw ax sahf jbe @encontrado @busca_siguiente: fstp dword ptr ds:[esp] add esp,4 l o o p @bucle2 mov ecx, 3E7 cmp ebx, 1 jbe @fin jmp @bucle1 @encontrado: mov eax, [esp+4] ; ENCONTRADO mov dword ptr ds:[eax], ebx add dword [esp+4], 4 mov dword ptr ds:[eax+4], ecx add dword [esp+4], 4 jmp @busca_siguiente @fin: nop; HA LLEGADO AL FINAL
El código es muy sencillo, pero hay que entender la Unidad de Punto Flotante. Lo que hace este código es generar 2 contadores: dividendo y divisor, números enteros desde el valor 1 hasta el 1000 (3E8hex). El registro ecx es el divisor y el registro edx es el dividendo. La primera instrucción push 30C0A0 guarda en la pila este valor que es la dirección donde se escribirán todos los datos encontrados. La instrucción push 3A03126F pasa posteriormente a la pila de punto flotante convirtiéndose en 0,0005 en la siguiente instrucción fld, y es el margen que voy a poner para que escriba los valores. fldpi carga en la pila de la Unidad de Punto Flotante el número π y es el que se va a utilizar para la resta.
Lo mejor para que veas lo que hace el código es que lo depures con un debugger como OllyDBG.
El ejemplo del código anterior tiene un dividendo y divisor máximos de valor 1000 y la precisión es de 0,0005. Tú puedes modificar ambos valores, por ejemplo, estas precisiones:
3556BF95 = 8e-07
33263D7C = 3.8e-08
34CFCCDB = 3.87e-07
3681E009 = 3.8e-06
3822580B = 0.000005
370637BD = 0.0000089
3851B717 = 0.00005
3A03126F = 0.0005
3BA3D70A = 0.005
Me dirijo en el código anterior a la dirección 0030C0A0, pongo un Breakpoint en 0030C02E y lo ejecuto. El resultado es el siguiente:
Como puedes ver, con una precisión en la resta de 0,0005, hay todavía muchos resultados. En la imagen anterior he resaltado el dividendo y divisor y la fracción más pequeña encontrada es: 267/85 ~ 3,1411764
Voy a indicarle más precisión en el resultado. Ahora le pongo: 0,0000089 y ejecutándolo solo me arroja 2 datos: 355/113 y 710/226. Esto significa que estas dos, son las fracciones de 3 cifras que más se aproximan al número π.
Ciertamente estas fracciones hubiesen sido imposibles de encontrar manualmente por el modo de ir reduciendo fracciones, pero para eso nos valemos hoy día del procesador... Como he dicho antes, el código se puede optimizar para que vaya más rápido (por ejemplo si quieres tener más precisión y mayor número de decimales) pero hubiese perdido la esencia de este tutorial. Tampoco he creado el código restante, por ejemplo, para mostrar una interfaz, botones... por lo mismo, aunque hacer esto último es muy sencillo. Creo que el código es muy interesante para trabajar con la Unidad de Punto Flotante sobre todo echa un vistazo a cómo se obtiene el flag de una operación de punto flotante, que veremos en otros tutoriales.