A la cuestión de los tampones (anillo)

"Si encuentra que los costos de desarrollo de la arquitectura son excesivos, considere cuánto puede costarle la arquitectura incorrecta"


- No puedo recordar exactamente la fuente

Una vez, "hace mucho tiempo, en una galaxia distante", compré el maravilloso libro de Charles Weatherly "Etudes for Programmers", en la introducción a la cual el autor confirmó la necesidad de estudiar ejemplos educativos y tareas antes de comenzar una programación independiente. Le recomiendo que encuentre este libro, lea el prefacio (y sin detenerse en él, lea el resto y resuelva los problemas que contiene), ya que no puedo justificar mejor la necesidad de tal práctica. Incluso si sigue mi recomendación y adquiere muchos conocimientos y habilidades prácticas al leer el libro, puede regresar y leer esta publicación, ya que está dedicada a varios otros temas. Y si no sigues mis recomendaciones, tanto más debes ir debajo del gato.

No hace mucho tiempo, en una publicación en la que lo regañé, expresé mi opinión sobre un RTOS doméstico, mencioné que la implementación del búfer de anillo en la conocida biblioteca mcucpp (y en ciertos aspectos, absolutamente maravillosa) no puede considerarse ideal. Trataré de explicar mi punto de vista e imaginar la implementación ideal (en la medida de lo posible en el mundo real). Nota: el texto ofrecido a su atención estuvo en "inacabado" durante bastante tiempo, y luego apareció un caso tan conveniente.

Continuamos desarrollando una biblioteca para trabajar con un dispositivo periférico, y somos los siguientes en la línea de gestión de memoria y almacenamiento en búfer (sí, todavía continuamos las operaciones preparatorias, pero sin ellas de ninguna manera). ¿De dónde viene la necesidad de organizar amortiguadores y qué tipo de animal es? El hecho es que una parte importante de la periferia tiene una velocidad limitada y el proceso de transmisión, que se inicia de una forma u otra, lleva un tiempo determinado, y a veces muy significativo, en comparación con la creación de otra porción de información para la transmisión. Por supuesto, antes de que este tiempo haya pasado, la siguiente transmisión no se puede realizar y, en consecuencia, no se puede iniciar.

Tenemos un caso clásico de un par escritor-lector con diferentes velocidades. Es simplemente imposible resolver este problema de manera general, ya que "con un exceso arbitrariamente pequeño, pero no cero, del flujo de solicitudes sobre el flujo de servicio, el tamaño de la cola tiende al infinito", y el infinito es fundamentalmente imposible. Pero un caso especial del problema, cuando tenemos ráfagas locales de solicitudes, pero en promedio el flujo de servicio puede hacer frente a la carga, se puede resolver una memoria intermedia de capacidad suficiente. Prestemos atención a la frase "capacidad suficiente", luego aprenderemos cómo calcularla, siempre y cuando el hecho de que esto sea fundamentalmente posible sea suficiente para nosotros.

Por supuesto, si la memoria intermedia es un requisito absoluto, no lo es. Para la información transmitida, puede usar un registro de bloqueo, pero con la información recibida es algo peor, tendrá que agregarse en algún lugar antes del procesamiento, si no toma las medidas apropiadas en el protocolo de nivel superior (la expresión mágica xon / xoff no nació desde cero), lo que no siempre es posible y, en cualquier caso, generalmente conduce a una limitación significativa de la velocidad de transmisión. También hay una implementación de hardware de memorias intermedias internas en dispositivos periféricos (al menos para un elemento), pero esto no siempre se hace y el tamaño de la memoria intermedia está estrictamente limitado desde arriba.

Por lo tanto, aún implementaremos el búfer de programa, para lo cual sería natural usar el método FIFO (es decir, la cola) para organizar dicho búfer, y la cola, a su vez, se implementa mejor en un búfer en anillo con dos punteros. Cuando escribo "mejor", esto no significa en absoluto que otras implementaciones (por ejemplo, una cola de referencia) sean imposibles o tengan fallas fatales que no sean fatales. Esta expresión solo significa que la implementación no será demasiado complicada y bastante efectiva, aunque otros pueden tener ventajas innegables sobre ella, por lo que tendrán que pagar por algo, porque DarZaNeBy.

Dado que es muy poco probable que su modelo MK tenga una implementación de hardware de un dispositivo de uso tan general (los módulos periféricos individuales pueden tener sus propios buffers de anillo, pero no tienen nada que ver con el tema de esta publicación), tendremos que crear un buffer de anillo en la memoria lineal (implementar en vector, este es, de hecho, el único objeto natural en la memoria direccionable), y para esto, se requerirá un índice de búfer (o tal vez incluso dos índices, pero más sobre eso más adelante). En mi opinión, un búfer circular con dos punteros (índices) es la única forma aceptable de implementar una cola en un vector, pero hay diferentes puntos de vista sobre este tema y vi con mis propios ojos una implementación en el estilo de "x1 = x2; x2 = x3; ... x8 = nuevo símbolo ", si quieres, no lo consideraré tan exótico. El hecho de que el fragmento dado pueda tener derecho a existir en una situación específica y muy limitada no lo hace aceptable en general.

Consideraremos la implementación correcta del módulo del programa para organizar el puntero y, para comenzar, prestaremos atención a la primera palabra en la definición. La diferencia entre un código correcto y uno incorrecto no es solo porque el código correcto no contiene errores, aunque este es un requisito absoluto. Incluso el código que realiza completamente sus funciones puede ser incorrecto si es incomprensible, o si hay una opción que no es menos clara, pero se ejecuta más rápido o se ejecuta tan rápido, pero más claramente escrito, por lo que el concepto de corrección es algo relativo. Continuamos considerando nuestro ejemplo de implementación de búfer, que nos permitirá demostrar la diferencia entre diferentes grados de corrección.

Antes de pasar a la esencia, un punto importante sobre la discusión adicional. Quiero decir que su compilador siempre está activado en un nivel de optimización diferente de cero (-O2), por lo que no tenemos que pensar en mejoras menores como 1) modificación del prefijo versus postfix, o 2) usar los resultados de la operación anterior, o 3) la diferencia entre el incremento y la suma unidades, etc., suponemos que el compilador hará mucho por nosotros. Por supuesto, esto no es una suposición estricta, pero de lo contrario tendremos que sumergirnos en las entrañas del ensamblador, que en nuestro tiempo no es la corriente principal.

Permítame recordarle que se nos indicó que implementemos el índice (puntero) del búfer en anillo, es decir, necesitamos crear el comportamiento de una variable que se ejecuta secuencialmente a través de una serie de valores, desde algunos iniciales hasta otros finales . Suponga de inmediato que el valor inicial será cero, de lo contrario tendremos que escribir un código más o menos correcto, y esto contradice los objetivos educativos y no tenemos prisa, y el último es Max.

Este comportamiento de la variable se puede implementar utilizando la siguiente construcción:

volatile int Counter = 0; Counter = (++Counter) % (Max+1); 

y es precisamente ese código el que podemos ver en muchos (es decir, muy a menudo) casos. Lo que está mal: bueno, en primer lugar, durante algún tiempo (desde la realización de la operación de incremento hasta la asignación del resultado) nuestra variable será mayor que el valor máximo permitido y, si en ese momento se produce una interrupción que debe tener en cuenta el valor de esta variable, entonces predigo personalmente No presumo los resultados. Por lo tanto, reescribimos el programa:

 int Counter=0; Counter = (Counter + 1) % (Max + 1); 

Hemos eliminado un error, y el código (en adelante me referiré al código "código" significa que el código ejecutable generado por el compilador) no se ha alargado y ya no se ejecuta (de hecho, se ejecuta más rápido, sino solo porque en la primera versión la palabra volátil se usa completamente redundante en este caso), y no se ha vuelto menos clara (más bien, aún más clara, pero esto es cuestión de gustos).

Nota necesaria sobre volátil: esta directiva es necesaria si queremos evitar la optimización del código que conduce a una ejecución incorrecta, y en este caso particular (cuando el valor de la variable no cambia fuera del alcance del módulo y no hay entradas secuenciales en él) (directiva ) completamente redundante. Le recomiendo que mire el código generado para ambas opciones en godbolt.org. Por qué no debe abusar de la directiva volátil, a diferencia de la palabra clave estática, que se recomienda utilizar siempre que sea posible. Bueno, en primer lugar, prohibimos la optimización, es decir, el código definitivamente no será más rápido (lo más probable es que se haga más grande y más lento, pero preferimos formulaciones estrictas). Y en segundo lugar, en este caso particular, esta palabra es engañosa, ya que en relación con nuestro programa, el valor del contador no puede cambiar de ninguna manera fuera de nuestro control. En un programa que lee su valor, es decir, en la implementación del búfer en anillo, puede considerar el contador como mutable fuera del módulo, y allí es cuestionable, por lo tanto, este atributo simplemente no es aplicable al contador. Si una variable se debe interpretar de manera diferente en diferentes módulos, nuestros servicios se deben combinar, si estamos hablando de organizar una sección crítica, por ejemplo, al implementar una transacción u operaciones atómicas, entonces esta directiva no da nada en absoluto.

Volvemos al código y vemos que el programa todavía está mal, lo que pasa, y el hecho es que no es lo que necesitamos (ver la descripción de la tarea), sino algo más (calcula el resto de la división), solo los resultados emparejar Bueno, creemos que sí (no lo creo, pero los autores del código ciertamente), que los resultados coinciden, de hecho, en el caso general, no coinciden, solo tuvimos suerte con el rango de la variable (valores positivos). Además, el proceso de ejecución del código es más largo de lo que podría hacerse, ya que en el mejor de los casos tenemos la operación de división de enteros (si es parte de los comandos de nuestra arquitectura), y no se realiza de ninguna manera en un ciclo de procesador (un valor característico de 10 ciclos para arquitectura de 8 bits), y en el peor de los casos, veremos la llamada al procedimiento de división desde la biblioteca estándar (y bueno, si es una división corta), el tiempo de ejecución será de decenas de ciclos de reloj.

Entonces, ¿por qué todavía es posible encontrar un enfoque tan completamente equivocado tan a menudo? Aquí, desde la audiencia, me dicen que con el valor de Max + 1, que es una potencia de dos, el compilador adivinará en lugar de la operación de división, colocará la operación de multiplicación bit a bit en la máscara correspondiente (igual a Max), que se realizará muy rápidamente y todo estará bien.

Estoy de acuerdo con esta declaración y adopto este enfoque, si no fuera por las siguientes circunstancias:

  • esto solo es posible para Mach estáticamente definido en la etapa de compilación,
  • esto solo sucede cuando la optimización está habilitada,
  • esto solo sucede cuando Mach cumple esta condición,
  • Esto no ocurre para todos los tipos cardinales.

Además, es en este caso particular (cuando la variable se define como un signo), además del comando de multiplicar (lógico) por la máscara, se generará un comando de comparación con cero y una rama para valores negativos, y aunque esta rama nunca será para nuestro rango se ejecutará, ocupará espacio en la memoria (y en el caso de una función sustituible, tomará varias veces) y llevará tiempo realizar la operación de comparación, si no lo cree, nuevamente vamos al sitio especificado y lo veremos por usted mismo. Otro argumento a favor de los cardenales sin firmar, al que recientemente dediqué una publicación completa.

Por lo tanto, si queremos usar la multiplicación lógica con una máscara (obtenida al optimizar el cálculo del resto), entonces deberíamos reescribir el módulo en consecuencia:

 typedef uint8_t Counter_t; typedef int8_t sCounter_t; static inline Counter_t NextCounter(const Counter_t Counter) { #if IS_POWER2(Max + 1) return (Counter + 1) & Max #else return (Counter + 1) % (Max + 1); #endif }; 

En esta versión, todo es completamente claro y controlable y todo es cierto (aunque persisten algunas deficiencias, pero ahora son obvias y no están enmascaradas), por lo tanto, es correcto, aunque es más correcto y ahora las buscaremos. El principal inconveniente, en mi opinión, es una violación del principio KISS, ya que el uso de la operación restante por división descuida completamente este principio. Por lo tanto, ahora destruiremos todas las deficiencias de un solo golpe (no te preocupes por su destino, renacerán 100,500 veces, porque no todos los programadores de Arduino leen mis publicaciones).

Pero primero, una ligera desviación hacia un lado. ¿Cómo podemos implementar una verificación de la potencia de dos (un número binario se puede representar como {0} 1 {0}) que acabamos de usar

no espiar
#define IS_POWER2 (N) ((((((N) - 1) & (N)) == 0)

¿Y cómo podemos implementar la verificación de que un número es una secuencia correcta de unidades {0} 1 {1} en notación binaria? Una opción es obvia

 #define IsRightSequence(N) IsPower2 ((N) + 1) 

y el segundo es trivial

 #define IsRightSequence(N) ( (((N) + 1) & (N)) == 0) 

Nota: no puedo evitar recordar el magnífico teorema: "Un número trascendental en un grado trascendental siempre es trascendental, a menos que lo contrario sea obvio o trivial".

¿Y cómo podemos verificar que un número sea una secuencia de unidades {0} 1 {1} {0}

 #define IsSequence(N) IsPower2( (N) ^ ((N) << 1)) 

Y finalmente, cómo seleccionar el número menos significativo de un número (no sé por qué esto podría ser necesario, pero será útil)

 #define LowerBit(N) ((((N) - 1) ^ (N)) & (N)). 


Pero se le ocurrió lo que puede ser útil
 #define IsRightSequence(N) (IsSequence(N) && (LowerBit(N) == 1)) 

Una observación curiosa: estas macros no son del todo correctas, resulta que 0 es una potencia de dos y una secuencia correcta (por supuesto, una secuencia también), lo cual es un poco extraño. Pero 1 es todos estos objetos con bastante razón, por lo que parece que cero solo debe considerarse por separado. Otra propiedad interesante de estas macros es que no hacemos suposiciones sobre la longitud del argumento, es decir, funcionan correctamente con cualquier tipo cardinal.

Hay un libro maravilloso, Trucos para programadores, donde puede encontrar las macros mencionadas y muchas otras tareas igualmente divertidas e instructivas, le recomiendo leerlo, especialmente porque no contiene demasiadas letras.

Pero volvamos a nuestro índice de búfer en anillo. Dimos la solución correcta, pero prometimos aún más correctamente, lo que significa que nuestra última solución tiene fallas (quién lo dudaría). Uno de ellos, la longitud del búfer debe determinarse estáticamente en la etapa de compilación, el segundo, en caso de una longitud fallida, el tiempo de ejecución es muy largo y todavía hay un cierto número de errores en una parte relativamente pequeña del programa, lo que nos hace recordar un chiste sobre 4 errores al escribir la palabra "más". Los eliminaremos a todos (algunos se dejarán para más adelante) e inmediatamente, para lo cual, finalmente, escribiremos la solución al problema original como es:

 static inline Counter_t NextCounter(const Counter_t Counter) { if ((Counter + 1) > Max) { return 0; } else { return Counter + 1; }; }; 

(Como ya entendió, soy partidario de los corchetes egipcios y no hay nada que hacer al respecto).

Prestemos atención al hecho de que simplemente reescribimos la condición del problema desde un lenguaje natural en el lenguaje de programación elegido, por lo que resulta extremadamente claro y comprensible. ¿Es posible mejorarlo? Sin duda, pero solo desde el punto de vista de la velocidad del código, ya que simplemente no hay otras deficiencias para esta solución (no hay deficiencias obvias, de hecho existen y las eliminaremos con éxito).

Evaluemos la complejidad computacional de esta solución: suma con unidad (1) y comparación (2) siempre, luego asignando cero (1) (raramente) o sumando (1) (casi siempre), lo que da 1 + 2 + 1 + Δ ~ 4 elemental operaciones y memoria cero. Es posible que un buen compilador en el modo correcto haga ciertas optimizaciones y reduzca el tiempo de ejecución del código, pero es mejor que lo hagamos explícitamente. Aquí está la siguiente opción:

 static inline Counter_t NextCouner(const Counter_t Counter) { register sCounter_t Tmp; Tmp = (Counter + 1); if (Tmp > Max) { Tmp = 0; }; return Tmp; }; 

Evaluamos la complejidad - suma y comparación siempre, asignando cero (raramente) - aproximadamente 3 operaciones y un elemento de memoria. De hecho, la versión anterior también tenía un elemento de memoria (implícito), por lo que tenemos una ganancia neta en una operación elemental. Además, la versión anterior tenía dos inconvenientes más: 1) violó el principio DRY (calculó el aumento en uno dos veces) y 2) tuvo más de un punto de salida, lo que no es bueno. Tampoco perdimos la comprensión, es decir, logramos matar a un montón de conejos de un disparo, y tampoco gastamos ningún cartucho, es solo una historia al estilo del Barón Munchausen.

Tenga en cuenta que no if ( (Tmp = Counter + 1) > Max) construcción if ( (Tmp = Counter + 1) > Max) , aunque contiene una instrucción explícita para el compilador para tratar de no hacer transferencias redundantes. Esto es aromatizante en la forma más flagrante, simplemente no me gusta el valor devuelto por el operador de asignación e intento evitar usarlo. No puedo explicar la razón de este fuerte sentimiento, según Freud, es muy probable que sea un trauma psicológico en la infancia. Los compiladores modernos son bastante capaces de llevar a cabo una optimización simple por sí mismos, y además, también agregué un calificador de registro, por lo que el código de mi versión y el correcto (desde el punto de vista del lenguaje C) coincidirán. Sin embargo, no limito en absoluto su libertad para usar el método que le parezca preferible.

Continuamos mejorando, porque no hay límite para la perfección y todavía no la hemos alcanzado. Para lograrlo, reformulamos un poco el problema original y dejamos solo que el requisito de la variable esté en el rango de valores, sin indicar la dirección del cambio. Este enfoque le permite reescribir el programa de la siguiente manera

 static inline Counter_t NextCouner(const Counter_t Counter) { register Counter_t Tmp; Tmp = (Counter - 1); if (Tmp < 0) { Tmp = ; }; return Tmp; }; 

A primera vista, nada ha cambiado mucho, pero, sin embargo, obtenemos una ganancia en el tiempo. Por supuesto, no debido al hecho de que la operación de disminuir por uno funciona más rápido que la operación de aumentar por él (aunque escuché una versión similar), sino debido a las peculiaridades de la comparación. Si en las versiones anteriores consideraba la comparación como 2 operaciones elementales (primero restamos y luego tomamos una decisión), entonces en este caso el resultado de la operación anterior se usa para tomar una decisión directamente y la comparación toma una operación elemental, lo que lleva a dos operaciones siempre y una asignación (raramente) y guardamos una operación (sin perder nada), como dice el refrán, "un poco, pero agradable". ¿Es la solución resultante ideal? Desafortunadamente, no. Es ligeramente inferior a la solución con una máscara (que requiere exactamente 2 operaciones elementales) en términos de velocidad y este es quizás su único inconveniente.

Existe una solución aún más rápida: simplemente aumente (disminuya) el valor del contador y no haga nada más, pero solo es posible en el caso en que el valor máximo coincida con el valor más representativo en el tipo aceptado. Para un contador de 8 bits (es decir, como uint8_t), será 255, luego escribimos Counter = Counter + 1 y confío en que escribir Counter + = 1 o ++ Counter es completamente opcional, aunque muchos son y escribirán y tendrán toda la razón. Si no consideramos seriamente la versión sobre la necesidad de guardar caracteres (ya que la primera opción es la más larga), entonces esto no tiene sentido, al menos si estamos escribiendo un programa para arquitectura ARM o AVR (para otros que simplemente no verifiqué, sospecho que el resultado será lo mismo) bajo el compilador GCC (el autor entiende que está escribiendo el programa en el editor del entorno de programación integrado, esto es solo una revolución del habla del pasado cuando las computadoras eran grandes y la memoria pequeña), y con la optimización activada en cualquier nivel, porque El código dado será absolutamente idéntico.

Los compiladores modernos son muy, muy avanzados en términos de optimización y generan un código realmente muy bueno, por supuesto, si ha habilitado el modo correspondiente. Aunque estoy dispuesto a aceptar que tales construcciones de lenguaje no hacen daño y pueden ser útiles bajo ciertas condiciones, lo único que noto es que las expresiones de Counter ++ (en este caso particular, por supuesto) deben evitarse sin ambigüedad, ya que está destinado a situaciones completamente diferentes y puede dar lugar a código más lento, aunque opcional.

Otra pregunta es que un búfer de 256 elementos no siempre es aceptable, pero si tiene suficiente memoria, ¿por qué no? Con esta implementación, si puede alinear el búfer con el borde de la página, entonces el acceso a los elementos se puede hacer muy rápido al eliminar la operación de cambiar de índice a índice (la palabra clave de unión le indicará la implementación de dicha función, no la traeré para no aprender malo), pero esta es una decisión muy, muy específica con un fuerte apego a la arquitectura, que está peligrosamente cerca de trucos en el peor sentido de la palabra, y este no es nuestro estilo.

Por supuesto, nadie nos prohíbe escribir un contenedor que llame a este o aquel método dependiendo del valor de los valores de contador máximos (y mínimos, ya que muchos métodos simplemente no funcionan con un mínimo distinto de cero), ya he propuesto los principios básicos de tal solución, por lo que Vamos a ofrecer esto como un ejercicio.

En resumen, para resumir, reuniremos diferentes implementaciones de trabajo con un índice de anillo y evaluaremos sus propiedades.
MétodoVersatilidadPlazo de entrega
±0 (1)1
±%1 (7)2
+ si3 (cualquiera)3.x
- si3 (cualquiera)2.x

La segunda línea entre paréntesis muestra el número de valores de tamaño del búfer (no superior a 256) para los que esta implementación está disponible, pero queremos decir que un búfer de tamaño 0 no nos interesa.

Como puede ver en esta tabla, DarZaNeBy (mi expresión favorita, como puede haber notado), y las ventajas se compran a costa de desventajas, lo único que se puede afirmar inequívocamente es que el incremento con la verificación tiene un competidor más exitoso en forma de disminución con verificación y no pasa a la siguiente ronda bajo ninguna circunstancia

Una nota necesaria: hay lenguajes de programación en los que no tendríamos que pensar en la implementación del índice, sino que simplemente podríamos usar el tipo de intervalo. Desafortunadamente, no puedo considerar óptima la implementación de estas construcciones en el código, ya que estas construcciones (y estos lenguajes) no están destinados a la optimización en tiempo de ejecución, pero es una pena.

Entonces, hicimos el módulo correcto (qué nombre tan fuerte para la función en línea) para trabajar con el índice, y ahora estamos listos para comenzar a implementar el búfer en anillo.

Y para empezar, debemos decidir qué queremos exactamente de este objeto de programa. Es absolutamente necesario poder colocar un elemento de datos en un búfer y extraerlo: dos métodos principales, una especie de captador y definidor. Es teóricamente posible imaginar un búfer sin uno de estos métodos, o incluso sin ambos (se puede imaginar poco teóricamente), pero el valor práctico de tal implementación es una gran pregunta. La siguiente funcionalidad necesaria, verificar la información, se puede implementar como un método separado o como un valor especial (o atributo) devuelto por la lectura. Por lo general, prefieren el primer método, ya que resulta más comprensible y no demasiado caro.
Pero verificar que el búfer esté completo ya es una gran pregunta: esta operación requerirá tiempo adicional, que siempre se dedicará a la grabación, aunque nadie nos obliga a usarlo, así que déjelo. No necesitamos nada más del búfer, recordemos esta frase para el futuro.

Volver a la implementación. Necesitamos un lugar para almacenar los elementos de la cola y dos índices: uno para escribir en el búfer y otro para leerlo. Cómo exactamente obtendremos este lugar (y estos indicadores) es un tema para una discusión por separado, por ahora dejamos este momento fuera de paréntesis y creemos que simplemente los tenemos. Algunos (incluidos los autores del libro "Programación para matemáticos", que respeto, recomiendo leerlo) también usan el contador de lugares rellenos, pero no haremos esto e intentaré mostrar por qué esto es malo.

Primero, sobre los índices: notamos de inmediato que estos son índices, no indicadores, aunque a veces me permití llamarme así. ( ), ( )- , , , . ( 256 ), , , ( , 8 , , 4- ), , , ( , ).

, 51 ( ) 2 ( ) 3 ( ), , , . , , GCC x51, AVR .

Además, muchos trucos que aumentan la velocidad para obtener el siguiente valor no estarán disponibles al usar el puntero. Y si también tiene en cuenta la opinión de que los punteros son más difíciles de entender (no es que esta opinión me pareciera correcta, pero existe), entonces la elección es clara: índices.

Pero, ¿qué deben mostrar exactamente los índices? Aquí, el alcance de la imaginación es ilimitado dentro del tamaño del búfer Max (e incluso más que eso), pero un conjunto muy pequeño de opciones tiene un significado práctico. Para el índice de grabación, hay dos posibilidades: 1) indicar el lugar donde se grabó el último elemento y 2) indicar el lugar donde se grabará el siguiente elemento. Dado que inmediatamente después de crear la cola, la primera opción parece un poco extraña, luego elegimos la segunda, especialmente porque esto nos promete una ganancia tangible en el futuro. Para el índice de lectura, asumimos de inmediato que apunta al elemento que se leerá la próxima vez que se lea. Inmediatamente hay un criterio simple (en el sentido de verificación) de que la cola no está vacía: los índices no son iguales. Pero surge el segundo problema: si ponemos en cola exactamente los elementos de Mach,entonces los índices coincidirán y no podremos distinguir esta situación de una cola vacía.

(«, ») , . — 1) , 2) ( , ) 3) , , 4) 256 , 5) ( ), . , , , , .

Solo necesitamos evitar una situación en la que los índices puedan coincidir después del próximo registro (el hecho de que puedan coincidir después de la lectura es obvio), y para esto necesitamos limitar el número posible de elementos en el búfer a 1 menos de lo posible. Aquí está su implementación:

 #define NeedOverflowControl YES typedef uint8_t Data_t; static Data_t BufferData[Max]; static Counter_t BufferWriteCounter=0, BufferReadCounter=BufferWriteCounter; void BufferWrite(const data_t Data) { BufferData[BuffWriteCounter] = Data; register counter_t Tmp = NextCount(BufferWriteCounter); #if (NeedOverflowControl == YES) if (Tmp == BufferReadCounter) {BufferOverflow();} else #endif { BufferWriteCounter = Tmp; } }; 

Hay una ligera incorrección en la función anterior, propongo encontrarla y arreglarla por mi cuenta, aunque ... todavía la hay, pero continuaremos:

  inline int BufferIsEmpty(void) { return ( BufferReadCounter == BufferWriteCounter ); }; inline int BufferIsFull(void) { return ( BufferReadCounter == NextCounter(BufferWriteCounter) ); }; #define DataSizeIsSmaller (sizeof(data_t) < sizeof(counter_t)) data_t BufferRead(void) { #if DataSizeIsSmaller register data_t Tmp = BufferData[BufferReadCounter]; #else register counter_t Tmp = BufferReadCounter; #endif BufferReadCounter = NextCount(BufferReadCounter); #if DataSizeIsSmaller return Tmp; #else return BufferData[Tmp]; #endif }; 

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

, , — , , , .

, ( )

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

2) — , .

:

3) 4) , (« »). — , , ( N N+1 ) , ?

3) ,

4) .

— « », - , — . 3, ( ), , .

— , ( , ),

5) — , , , , — , .

— , , .

, , , , , , , , , , , . , 4 , , . MRSW (Multi-Reader Single-Writer) «The Art of Mulpiprocessor Programming» ( , ) ( ) . — , , .

MRMW , «» (, , « » ). , , , , . , , , (, , — , , , ), .

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

 typedef uint8_t data_t; static data_t BufferData[Max]; static counter_t BufferWriteCounter=0, BufferReadCounter=WriteCounter; static int8_t BufferHaveData = 0; void BufferWrite(const data_t Data) { if ((BufferWriteCounter == BufferReadCounter) && (BufferHaveDataFlag == 1)) {BufferOverflow();} else { BufferData[BufferWriteCounter] = Data; BufferHaveDataFlag = 1; BufferWriteCounter = NextCounter(BufferWriteCounter); }; }; inline int BufferIsEmpty(void) { return ((BufferReadCounter==BufferWriteCounter) && (BufferHaveDataFlag == 0));}; data_t BufferRead(void) { register counter_t Tmp; Tmp = BufferReadCounter; BufferReadCounter = NextCount(BufferReadCounter); if (BufferReadCount == BufferWriteCounter) { BufferHaveDataFlag = 1; }; return BufferData[Tmp]; }; 

, , , , , .

, ( 0 1, , , ), , , , , , ( ), , ,

-
 typedef (NoBufferHaveData= 0, BufferHaveData =1) BufferHave DataFlag_t; BufferHaveData_t BufferYaveDataFlag; inline void BufferHaveDataFlagSet(void) {BufferHaveDataFlag = NoBufferHaveData;}; inline void BufferHaveDataFlagClr(void) {BufferHaveDataFlag = BufferHaveData;}; inline int BufferHaveDataFlagIsSet(void) {return (int)(BufferHaveDataFlag == BufferHaveData);}; 


, , 0 1, . , , , 0 1. , , , , BufferFullFlag , BufferIsNotEmptyFlag . , KISS , , , , , « ».

, , .

, , .

PS , , :

  1. — (, , ), — , , , , .
  2. .
  3. .
  4. 2 .
  5. , ( ) , , .
  6. ,

     return ((_writeCount - Atomic::Fetch(&_readCount)) & (size_type)~(_mask)) != 0; 

    — , , ,

     size_type(~(_mask)) 

    .

PPS , .

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


All Articles