A la cuestión de las transformaciones y otras operaciones.

Blue Caterpillar: Bueno, no nos decepcionarás. Nos sentamos, lo sabemos: están esperando nuestra transformación. Que? Pero nada! Nos sentamos, fumamos, esperamos ...
Muñeca Alice: ¿Qué?
Oruga azul: ¡qué, por qué! De transformaciones. La casa en humo, el humo en una dama, y ​​la dama en una madre. Ahí tienes. No interfieras, no saltes hacia adelante, de lo contrario, tú mismo te convertirás prematuramente en una especie de mariposa.


Al revisar el código en uno de los foros dedicados a Arduino, encontré una forma divertida de trabajar con un número de coma flotante (PT). El segundo nombre común para los números en este formato es coma flotante, pero la abreviatura (PP) que surge en este caso personalmente me causa asociaciones completamente diferentes, por lo que utilizaremos esta opción. La primera impresión (del código que vi) es qué tipo de basura se escribe aquí (debo decir que la segunda es la misma, aunque hay matices, pero más sobre eso más adelante), pero surge la pregunta: ¿cómo es realmente necesario? La respuesta a la que se da en Texto adicional.

Primera parte: preguntas


Formulamos el problema: necesitamos imprimir en la consola (convertirlo en una representación simbólica) un número de coma flotante, sin usar las opciones de impresión, que están destinadas para este propósito. ¿Por qué queremos hacer esto por nuestra cuenta?

  1. El uso del formato% f implica conectar la biblioteca para trabajar con un punto flotante y una versión extendida de la función prntf (o más bien, hace que sea imposible usar su versión truncada), lo que conduce a un aumento significativo en el tamaño del módulo ejecutable,
  2. una solución estándar requiere un tiempo considerable (siempre funciona con un número de doble precisión), lo que puede ser inaceptable en esta situación particular,
  3. Bueno (por último, pero no menos importante), es simplemente interesante.


Para comenzar, considere la opción propuesta en el material anterior, algo así como:

for (float Power10=10000.0; Power10>0.1; Power10/=10.0; ) {char c=(int)(Fdata/Power10); Fdata -=Power10*c; }; 

y estamos de acuerdo en que él resuelve completamente el problema. Además, esta no es una mala opción, ya que su velocidad puede ser bastante aceptable. Echemos un vistazo más de cerca en este momento: vemos la división de los números de PT, pero si profundizamos en la esencia del problema, resulta que es casi tan rápido como la división de enteros de la profundidad de bits correspondiente. De hecho, antes de evaluar el rendimiento del algoritmo, debe evaluar el rendimiento de varias operaciones elementales, lo que haremos.

Segunda parte: evaluación del desempeño de las operaciones elementales


La primera operación interesante es la suma (resta, en el sentido del tiempo empleado, son equivalentes) de enteros de números y podemos suponer que se necesita una unidad de tiempo (ciclo de reloj) con la siguiente advertencia: esto es cierto solo para datos "nativos". Por ejemplo, para la serie MK AVR es una palabra de 8 bits, para MSP430 es una palabra de 16 bits (y, por supuesto, de menor tamaño), para Cortex-M es una palabra de 32 bits y así sucesivamente. Entonces, la operación de sumar números con una longitud de H veces mayor que la nativa se puede estimar como H ciclos. Hay excepciones, por ejemplo, AddW en los controladores AVR, pero no cancela la regla.

La siguiente operación es la multiplicación de enteros (pero no la división, difiere en términos de velocidad) y para él no todo es tan simple. En primer lugar, la multiplicación puede implementarse en hardware y, por ejemplo, en AVR MEGA requiere 2 ciclos de reloj, y en 51 mejorados hasta 6 (para multiplicar números nativos).

Pero considere el caso cuando no hay implementación de hardware y tenemos que implementar la multiplicación en forma de subrutina. Dado que al multiplicar números de bit H, se obtiene un producto de 2H bit, entonces la estimación de la versión clásica con turnos se puede encontrar de la siguiente manera: necesitamos turnos H del factor con 1 ciclo de reloj por turno, turnos H del segundo factor con una longitud de 2 H con 2 ciclos de reloj por turno, luego H tomará decisiones y , en promedio, N / 2 adiciones de números con una longitud de 2H, en conclusión, la organización de un ciclo de 2 medidas. Total + 2 + + 2 / 2 + 2 = 7 ticks, y realizar operaciones aritméticas con ellos solo requiere N ticks (wow eficiencia, aunque logramos sortear el motor).

Es decir, para multiplicar dos números de 8p por 8p MK, se requieren 56 ciclos, y para multiplicar números de 16p ya hay 112 ciclos (un poco menos, pero descuidamos el valor exacto), que es algo más de lo que queríamos. Afortunadamente, la dirección de los cambios se puede modificar y hay una forma única de multiplicación, que requerirá solo cambios H del número de dígitos 2H y adiciones H / 2 de números nativos, lo que mejora el tiempo de operación del algoritmo de multiplicación a 0 + 2 + 1 + 1/2 + 2 = 5.5 - por supuesto, no se puede comparar con la implementación de hardware, pero al menos algo de ganancia sin pérdida de funcionalidad. Hay mejoras en este algoritmo, por ejemplo, el análisis de 2 bits por ciclo, pero no cambian drásticamente la situación: el tiempo de multiplicación por órdenes de magnitud excede el tiempo de adición.

Pero con la división, la situación es peor: incluso la división implementada en hardware pierde casi el doble por la multiplicación, y hay MK con multiplicación por hardware, pero sin división por hardware. Bajo ciertas condiciones, la división se puede reemplazar por la multiplicación por el recíproco, pero estas condiciones son específicas y dan un resultado similar: se requieren dos iteraciones de multiplicación seguidas de la suma, por lo que hay una pérdida de 2 veces. Si implementamos la división como un subprograma, entonces N desplazamientos del divisor 2H de largo, H sustracciones del divisible 2H de largo, H desplazamientos del resultado, se requiere la organización 2H del ciclo, pero todo esto está precedido por la alineación, que tomará otros 5H ciclos, por lo que la cifra total es 2 + 2 + 1 + 2 + 5 = 12, que es aproximadamente 2 veces peor que la multiplicación.

Bueno, ahora consideraremos las operaciones de PT, y aquí la situación es algo paradójica: la operación de multiplicación requiere casi tanto tiempo como para los enteros (correspondientes a la capacidad de bits, como regla, 24 bits), ya que debemos multiplicar la mantisa y sumar las órdenes, la normalización no requerido Con la división también es bueno, dividir la mantisa y restar las órdenes, la normalización nuevamente no es necesaria. Por lo tanto, para estas dos operaciones, la pérdida en comparación con los enteros no es demasiado significativa, aunque tiene un lugar.

Pero la operación de suma y resta requiere, en primer lugar, la alineación de las órdenes (y estos son turnos y puede haber muchos, aunque hay matices), luego la operación en sí y (al restar) la normalización (al sumar, también, pero no requiere más de 1 turno ), que es un desperdicio de tiempo, por lo tanto, las operaciones de esta clase para PT son mucho más lentas que para los enteros, especialmente en términos relativos.

Volvamos a nuestras ovejas y aceptemos que, según las estimaciones anteriores, el método propuesto puede no ser demasiado largo, especialmente porque da el resultado de inmediato, pero tiene una limitación significativa: es aplicable a un rango muy limitado de valores de entrada de PT. Por lo tanto, buscará una solución universal (más o menos).

Inmediatamente haga una reserva de que nuestra solución no debe usar operaciones de punto flotante en general (de la palabra) para enfatizar los méritos de nuestra opción. Y a la pregunta perpleja de cómo aparecerá un número de este tipo si las operaciones no están disponibles, respondemos: puede aparecer, por ejemplo, al leer información de un sensor de luz (como en el ejemplo original), que produce datos en el formato PT.

Cómo se organiza exactamente el número de PT, puede encontrarlo fácilmente en numerosos sitios, hubo un artículo reciente sobre Habré, no debería haber problemas con esto. Sin embargo, una serie de preguntas son de interés para el formato PT en el estilo "si yo fuera el director", por qué esto es así, y no de otra manera. Daré mis respuestas a algunas de ellas, si alguien sabe más correcto, por favor comente.

La primera pregunta es ¿por qué la mantisa se almacena en código directo y no en código adicional? Mi respuesta es porque es más fácil trabajar con una mantisa normalizada con un bit oculto (opcional).

La segunda pregunta es por qué el pedido se almacena con un desplazamiento, y no de otra manera. Mi respuesta es porque en este caso es fácil comparar los módulos de dos PT como enteros, con otros métodos es más complicado.

La tercera pregunta es ¿por qué el signo negativo está codificado por uno en lugar de cero, porque entonces sería posible comparar simplemente los dos puntos como enteros? Mi respuesta es que no lo sé, es solo "es tan aceptado aquí".

Parte tres - Explicaciones requeridas


En el párrafo anterior, podría dar términos incomprensibles, un poco sobre la representación de números. Por supuesto, son diferentes, de lo contrario no habría necesidad de discutirlos. Inmediatamente, notamos que en la memoria de MK (lo mismo es cierto sobre las computadoras, aunque no soy tan categórico sobre las arquitecturas más modernas, son tan complicadas que todo puede esperarse) no hay números, solo hay unidades de almacenamiento elementales, bits agrupados en bytes y más en palabras. Cuando hablamos de la representación de un número, significa que interpretamos un conjunto de bits de una longitud específica de una forma u otra, es decir, establecemos una ley por la cual podemos encontrar un cierto número correspondiente a un conjunto de bits dado, y nada más.

Se pueden inventar innumerables leyes de este tipo, pero algunas de ellas tendrán una serie de propiedades útiles en términos de realizar diversas operaciones, por lo que se aplicarán con mayor frecuencia en la práctica. Una de estas propiedades, que está implícitamente implícita, por ejemplo, es el determinismo, y la otra es la independencia del medio ambiente, propiedades que son, a primera vista, obvias, aunque hay matices. Otras propiedades del tipo de correspondencia uno a uno ya son tema de discusión y no siempre tienen lugar en una representación concreta. El tema de representar números en sí mismo es inusualmente fascinante; para Knut (en el Volumen Dos) se revela completamente, de modo que va más allá de las profundidades y pasamos a la superficie.

Bajo el supuesto de que el conjunto de bits tiene una longitud n (los numeramos en una fila de 0 a n-1) y se ponderan uniformemente con un paso de 2 y el bit menos significativo (con el número 0) tiene un peso de 1 (que, en términos generales, no es necesario en absoluto, simplemente Nos acostumbramos a tales cosas, y nos parecen obvias) obtenemos una representación binaria del número, en la cual la fórmula de reducción se ve así: el número que muestra el conjunto de bits (2) = (0)*2^0 + (1)*2^1 + ... + (-1)*2^(-1) o en forma de cascada 2() = (0)+2*((1)+2*(...+2*((-1))..))) , en adelante, B (k) denota un bit con el número k. Tenga en cuenta que debajo una vista diferente no impone ninguna restricción en la ubicación de los bytes de número en la memoria, pero sería más lógico colocar el byte bajo en las direcciones más bajas (así de fácil y naturalmente resolví el "argumento eterno de los eslavos entre ellos" con respecto a qué extremo es más conveniente para romper un huevo).

Con esta interpretación de un conjunto de bits de longitud n (= 8), obtenemos una representación de números del 0 al (2 ^ n) -1 (= 255) (en lo sucesivo entre paréntesis habrá un valor específico para un conjunto de 8 bits), que tiene un número de y propiedades útiles, por lo que se ha generalizado. Desafortunadamente, también tiene una serie de inconvenientes, uno de los cuales es que, en principio, no podemos representar números negativos en dicho registro.

Puede ofrecer una variedad de soluciones a este problema (la representación de números negativos), entre las cuales también hay importancia práctica, se enumeran a continuación.

La fórmula H = N2 (n) - desplazamiento (C) describe una representación con un desplazamiento, donde N2 es el número obtenido en notación binaria con n bits, y C es un valor preseleccionado. Luego representamos números de 0-C a 2 ^ (n) -1-C, y si elegimos C = 2 ^ (n-1) -1 (= 127) (esto es completamente opcional, pero muy conveniente), entonces obtenemos el rango de 0- (2 ^ (n-1) -1) (= - 127) a 2 ^ (n-1) (= 128). La principal ventaja de esta representación es la monotonía (además, aumento) en todo el intervalo, también hay desventajas, entre las que destacamos la asimetría (hay otras relacionadas con la complejidad de realizar operaciones en el número en esta representación), pero los desarrolladores del estándar IEEE 457 (este es el estándar para PT) convirtió esta falla en una virtud (usando un valor extra para codificar la situación nan), que una vez más enfatiza la fidelidad del refrán genial: “Si eres más alto que el oponente, esta es tu ventaja. Si el adversario es más alto que tú, entonces esta también es tu ventaja.

Tenga en cuenta que dado que el número total de combinaciones posibles de cualquier número de bits es par (si no tiene combinaciones prohibidas por razones religiosas), la simetría entre números representables positivos y negativos es fundamentalmente inalcanzable (o más bien, alcanzable, pero bajo ciertas condiciones adicionales, sobre las cuales, además) .

Representación en forma de código directo cuando uno de los bits (más significativo) representa el signo codificado del número H = (-1) ^ B (n-1) * P2 (n-1) tiene un rango de 0- (2 ^ (n-1) -1) (= -127) a 2 ^ (n-1) -1 (= 127). Es interesante notar que acabo de declarar la imposibilidad fundamental de la simetría, y aquí está claramente: el número positivo máximo representable es igual al módulo del número negativo mínimo representable. Este resultado se logra al tener dos representaciones para cero (00 ... 00 y 10 ... 00), que generalmente se considera la principal desventaja de este método. Esto es realmente un inconveniente, pero no tan terrible como comúnmente se cree, ya que hay otros más importantes que limitan su uso.

La representación de código inverso, cuando en la representación directa invertimos todos los bits del valor para números negativos H = (1-B (n-1)) * P2 (n-1) + B (n-1) * (2 ^ (n -1) -CH2 (n-1)): esto es de la definición, puede hacer una fórmula mucho más comprensible H = Ch2 (n-1) -B (n-1) * (2 ^ (n-1) -1), lo que nos permite representar números de 0-2 ^ (n-1) +1 (= - 127) a 2 ^ (n-1) -1 (= 127). Se puede ver que esta representación está desplazada, pero el desplazamiento cambia paso a paso, lo que hace que esta representación no sea monótona. Nuevamente, tenemos dos ceros, lo que no da mucho miedo, la ocurrencia de transferencia circular durante la adición es mucho peor, lo que crea ciertos problemas en la implementación de ALU.

Para eliminar el último inconveniente de la representación anterior es inusualmente simple, es suficiente cambiar el desplazamiento por uno, luego obtenemos = = 22 (n-1) -B (n-1) * 2 ^ (n-1) y podemos representar números de 0-2 ^ ( n-1) (= - 128) a 2 ^ (n-1) -1 (= 127). Es fácil ver que la representación es asimétrica, pero cero es único. Significativamente más interesante es la siguiente propiedad, "es completamente obvio que" la transferencia de anillo no ocurre para una operación de tipo suma, que es la razón (junto con otras características agradables) para la distribución universal de este método particular de codificación de números negativos.

Dibujemos una tabla de valores interesantes para varios métodos de codificación de números, denotando por H el valor 2 ^ (n-1) (128)
Pedacitos00..0011/0110..0011.11
H (n)0 0H-1 (127)H (128)2 * H-1 (255)
H (n-1)0 0H-1 (127)0 0H-1 (127)
Offset N-H + 1 (-127)0 01H (128)
Directo0 0H-1 (127)0 0-H + 1 (-127)
Reverso0 0H-1 (127)-H + 1 (-127)0 0
Además0 0H-1 (127)-H (-128)-1

Bueno, para concluir el tema, proporcionamos gráficos para las representaciones enumeradas, a partir de las cuales sus ventajas y desventajas son inmediatamente visibles (por supuesto, no todo lo que hace que uno recuerde el dicho interesante "La ventaja de la presentación gráfica de la información es visual, no tiene otras ventajas").

Cuarta parte: resolver el problema original (más vale tarde que nunca).

Pequeña digresión


Para empezar, quería imprimir el PT en formato hexadecimal (y finalmente lo hice), pero de manera bastante inesperada / completamente inesperada (necesitaba sustituirlo) me encontré con el siguiente resultado. ¿Qué crees que se imprimirá como resultado de ejecutar los operadores?

 printf("%f %x", 1.0,1.0); printf("%f %x",2.0,2.0); printf("%x %d",1.0,1.0); printf("%x %d",2.0,2.0); 

, también preste atención a la siguiente construcción y su resultado:

 printf("%x %x %f",1.0,1.0); 

No daré explicaciones a este fenómeno, "lo suficientemente inteligente".

Sin embargo, ¿cómo imprimimos correctamente la representación hexadecimal de PT? La primera solución es obvia: unión, pero la segunda es para los ventiladores de una sola línea printf ("% x", * ((int *) (& f))); (Pido disculpas si alguien se ofendió con corchetes adicionales, pero nunca pude, y nunca tuve la intención de recordar las prioridades de las operaciones, especialmente teniendo en cuenta que los paréntesis no generan código, por lo que continuaré haciendo lo mismo). Y aquí está, la solución de la tarea: vemos una cadena de caracteres, 0x45678, que determina de forma única el número deseado para nosotros, pero de tal manera que (no sé sobre ti, definitivamente) no podemos decir nada inteligible sobre este número. Creo que el académico Karnal, que podría haber señalado un error en la cinta perforada con el código fuente, habría abordado esta tarea, pero no todos están tan avanzados, por lo que continuaremos.

Intentaremos obtener información de una forma más comprensible.

Para hacer esto, volvemos al formato del PT (en adelante, considero solo flotante), que es un conjunto de bits del que puede extraer (según ciertas reglas) tres conjuntos de bits para representar tres números: signo (s), mantisa (m) y orden (p), y el número deseado codificado por estos números estará determinado por la siguiente fórmula: Cs * Chm * Chn. Aquí, los símbolos designan los números representados por el conjunto de bits correspondiente, por lo tanto, para encontrar el número deseado, necesitamos conocer las leyes por las cuales extraemos estos tres conjuntos del conjunto original de bits, así como el tipo de codificación para cada uno de ellos.

Al resolver este problema, recurrimos al estándar IEEE y descubrimos que el signo es un bit (principal) del conjunto original y la fórmula para codificar Cs = (- 1) ^ B (0). El orden ocupa los siguientes 8 bits altos, se escribe en código con un desplazamiento de 127 y representa una potencia de dos, luego Cn = 2 ^ (C2 (8) -127). Mantissa toma el siguiente orden de 23 dígitos y representa el número Chm = 1 + Ch2 (23) / 2 ^ 23.

Ahora tenemos todos los datos necesarios y podemos resolver completamente la tarea: crear una cadena con caracteres, que, con una cierta lectura, representará un número igual al codificado. Para hacer esto, debemos, mediante operaciones simples, extraer los números anteriores y luego imprimirlos, proporcionando los atributos necesarios. Suponemos que somos capaces de convertir un entero con no más de 32 bits en una cadena de caracteres, entonces esto es completamente sencillo.

Desafortunadamente, solo estamos al comienzo del viaje, ya que pocos lectores de esta publicación en el registro "+ 1.625 * 2 ^ 3" reconocen el número desafortunado, que está codificado por el decimal más común "13", y adivinan en el registro "1.953125 * 2 ^ 9 "el simple" 1E3 "o" 1 * 10 ^ 3 "o el muy familiar" 1000 "son capaces de unidades de personas en general, definitivamente no les pertenezco. Es extraño cómo sucedió, porque completamos la tarea inicial, que una vez más demuestra cuán cuidadosamente debe tratar las formulaciones. Y el punto no es que la notación decimal sea mejor o peor que la binaria (en este caso, deuce se basa en el grado), sino que estamos acostumbrados al decimal desde la infancia y rehacer a las personas es mucho más difícil que el programa, por lo que daremos nuestro entrada a lo más familiar.

Desde el punto de vista de las matemáticas, tenemos una operación simple: hay un registro PT = (- 1) ^ s * m * 2 ^ n, y necesitamos convertirlo a la forma PT = (-1) s '* m' * 10 ^ n '. Equiparamos, transformamos y obtenemos (una de las opciones posibles) soluciones s '= s', m '= m, n' = n * log (2). Si dejamos los corchetes la necesidad de multiplicar por un número explícitamente irracional (esto se puede hacer si el número está racionalizado, pero hablaremos de esto más adelante), entonces el problema parece estar resuelto hasta que veamos la respuesta, porque si el registro es como "+1.953125 * 2 ^ 9 "nos parece oscuro, el registro" + 1.953125 * 10 ^ 2.70927 "es aún menos aceptable, aunque parecía que no había nada peor.

— 10 '= * 10^{ * lg(2)}, '= [ * lg(2)], . (1.953125*10^0.7 0927)*10^2=«10*10^2», , , .

, :

  1. () (lg(2)) ( );
  2. ( );
  3. (10) (...);
  4. (« , ...»).

, , , 1. , * lg(2), «» , =0 ( =/lg(10)). , « », « ». . , , ' = * lg(2) * [lg(2) *256 + 1/2] / 256 , 1/2/77 = 1/144, , 1/100. — , . [4.501]=5, [4.499]=4 , , 0.002/4.5=0.04%, 1/4=25%. , , . , , , , , , .

Para nuestro caso, tal aproximación ideal será la función n '= n * 77/256.

Antes de continuar con el diseño del algoritmo, debemos evaluar la precisión que necesitamos. Como la mantisa es de 24 bits, el número representado tiene un error relativo de 2 ^ -24 = 2 ^ -4 * 2 ^ -20 = 16 ^ -1 * (2 ^ 10) ^ - 2 ~ (10) ^ - 1 * (10 ^ 3) ^ - 2 = 10 ^ -7, lo que significa 7 dígitos decimales exactos. Multiplicar dos números de 24 bits será suficiente para mantener la precisión en este rango (bueno, casi suficiente). Solo tenga en cuenta que la transición a números de 32 bits (ambos factores) reduce el error relativo en más de 100 (256) veces, este hecho será útil más adelante.

La fórmula más desagradable en términos de precisión calcula una nueva mantisa y se ve como

m '= m * 10 ^ {n * log (2)}

— 1) , 2) , , , , , . — , , , .

« , , »

q(10^x) = Δ(10^x)/10^x = (10^(x +Δx) — 10^x)/10^x = 10^Δx -1 = 10^(x*qx)-1,
10 ^ (x * qx)> ~ 10 ^ (x * 0) + (10 ^ (x * 0)) '* qx = 1 + x * ln (10) * 10 ^ (0) * qx = 1 + x * ln (10) * qx,

de aquí obtenemos

q(10x)=x∗ln(10)∗qx.

— , , =127, 292 , , .

, 24 32 ( , ), , (*lg(2)) 32 , , 1'292'914'005/2^32. , , (int)((lg(2)*float(2^32))+0.5), 04d104d42, , .

, , , , .
10 0 1 . , , , , , ''=lg(2)*i+(''-lg(2)*i), 2 , ( ), lg(2) 10^'' ( ).

, lg(2) , , . , , , , 10-7 9 , 1+9*2=19 32- , . '=*lg(2) , .

32- 1+19+1=21

— , — , . , — ( ) , , .

— — (2^8=256) ['] ( 10) {'} ( ), . — =*2^=*10^'*(2^/10^')=(*(2^/10^'))*10^'.

En el caso más simple, necesitamos 256 * 3 (un factor de corrección de 24 bits, ya no es necesario) + 256 * 1 (se garantiza que el orden en la base 10 es menor que el orden en la base 2) = constantes de 1kbyte. En este caso, nos queda hacer una sola multiplicación de 24 * 24 bits (lo más probable es que sea 32 * 32), lo que acelera significativamente el trabajo en comparación con la versión de este cálculo.

Veamos qué se puede hacer desde el punto de vista del ahorro de memoria (en este caso, nuevamente tenemos que pagar tiempo, por lo que estamos buscando un compromiso razonable). En primer lugar, si tenemos en cuenta el signo de pedido por separado, podemos administrar solo la mitad de la memoria requerida (de 256 bytes para el pedido 10) e invertir el resultado si es necesario. Desafortunadamente, con el factor de corrección, no será tan fácil, porque

2 ^ -n / 10 ^ -n '= 1 / (2 ^ n / 10 ^ n')! = 2 ^ n / 10 ^ n ',

una pena. Tenemos que dejar una tabla larga, o para indicadores negativos, dividir por una constante para indicadores positivos. Por supuesto, la división no es 18 multiplicaciones, pero aún así es exactamente equivalente en velocidad a dos multiplicaciones, por lo que el tiempo definitivamente se duplicará para ahorrar memoria dos veces, hasta 512 bytes. ¿Vale la pena? La pregunta no es simple, pero, afortunadamente, tenemos una forma mucho más hermosa que nos permite deshacernos del sufrimiento de la elección.

- , ( ) . ( )

=*2^=*2^(0+1)=*10^'*(2^(0+1)/10^')=*(2^0/10^')*2^1*10^',

0- , 1=-0. , .

— , 0 ? , — 10 . — 32*32, 24 , 8 8 . 256/8*4=32*4=128 — 8 .

0, , 32/2=16 , , ( ) .

, adafruit

 const UINT8 Bits[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; ... data = data | Bits[n]; 

con un comentario de que la operación 1 << n en el AVR lleva mucho tiempo. En mi publicación, ya mostré qué milagros hace el compilador con un parámetro constante, pero este no es el caso.

Me pareció dudoso que tomar una máscara de bits de una matriz sería más rápido que realizar las operaciones de cambio directamente y el análisis de código posterior (usando el sitio web godbolt, aunque es extremadamente improbable que su creador lea el Habr, sin embargo, una vez más, le traigo mi sincero gratitud) demostró que esto es realmente así.

El código generado por el compilador para ambas opciones (aquí está la opción correcta con turnos, teniendo en cuenta las características de la tarea, porque solo necesitamos 1 bit)

  ldi r18,lo8(4) sbrs r25,1 ldi r18,lo8(1) sbrc r25,0 lsl r18 sbrs r25,2 swap r18 

, , 8:7 8 (, , 16 — — « , , , »). — , : « „ 12 “ (» ", , ).



= * 2^=( * [/8]) * 2^(%8) * 10^[/8],

- - . , 32*32(24*24) . 32 10 , ( , ) .

— ,

const uint32_t Data[32] PROGMEM = { 0xF82345,… }

y el punto, por supuesto, no está en los atributos de la descripción de la matriz, sino en los datos en forma de números mágicos. Como los autores señalaron correctamente, definitivamente no es más tonto que yo, un código bien escrito es autodocumentado y, si escribimos la constante anterior (y el resto) en el formulario

 #define POWROUD(pow) ((uint8_t)((pow & 0x07)*log(2)+0.5)) #define MULT(pow) (2^pow / 10^POWROUND(pow)) #define MULTRAW(pow) (uint32_t((MULT(pow) << 24) +0.5)) #define BYTEMASK 0xFF #define POWDATA(pow) ((POWROUND(pow) & BYTEMASK)| (MULTRAW(pow) & (~BYTEMASK))) const uint32_t Data[(BYTEMASK/8)+1] = { POWDATA(0x00),POWDATA(0x08), ..POWDATA(0xF8)} 

entonces nadie nos enviará preguntas perplejas, y si alguien nos envía, definitivamente no podemos responderlas, todavía es inútil.

Podemos proponer una modificación de este método en el que se calculará una potencia adecuada de diez no para el borde derecho del segmento, sino para la izquierda y luego el resultado no se desplazará hacia la derecha para tener en cuenta la potencia de dos, sino hacia la izquierda. Desde el punto de vista de las matemáticas, los métodos son absolutamente equivalentes. Veamos el resultado:

1.953125 * 2 ^ 9 = 1.953125 * 2 ^ (8 + 1) = 1.953125 * 42949673/256/256/256 (2.56) * 2 * 10 ^ 2 = 10 * 10 ^ 2

ya es muy fácil encontrar 1000 aquí. Por supuesto, también tendríamos que convertir la mantisa y el orden obtenidos en cadenas, redondear cuidadosamente, ajustar el resultado al formato requerido, agregar un carácter, tener en cuenta casos especiales, etc., pero esto ya no es tan interesante, la parte principal transformaciones que realizamos.

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


All Articles