Более быстрая реализация кольцевого буфера

Рик Кимбалл
Чт 14 июля 2016 г., 17:21
Некоторое время назад я создал кольцевой буфер на основе шаблона (круглый буфер) с использованием шаблонов C ++ для MSP430 MCU. Он был быстрее, чем на C, использованный в Arduino/Energia. Я попробовал это на STM32Duino и сравнил скорость с Libmaple/Ring_buffer.H версия. Ringbuffer_t ниже, кажется, занимает 36% меньше времени.

Для меня время вставить предмет наиболее важно, так как я использовал его для буфера входящих серийных байтов. Я хотел, чтобы это было быстро. Вставка Maple Ring_buffer занимает около 500 секунд. Функция вставки в коде ниже (push_bach) занимает всего около 320 нано секунд.

(Кстати: это также подчеркивает, как накапливается процессор ARM 72 МГц, на MSP430 вставка занимает около ~ 1680 нс)

кольцо.час /* ringbuffer.h - yet another version of ringbuffer_t c++ template Desc: This template class creates a ringbuffer with arbitrary types. Provides push and pop methods for adding and removing items. Access function for checking the number of items in the buffer, capacity and full or empty methods provide safe access to the info. This version has been optimized for the msp430-gcc and stm32. It doesn't disable or enable any interrupts. It is safe nonetheless for use when there is a single writer and single reader. This is common when using an interrupt and the main line thread working together to move data in and out of peripherals on demand. Version: 1.0.3 Created: Jul-24-2012 Author: [email protected] Date: 02-28-2013 ========================================================================= Copyright © 2012-2016 Rick Kimball This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Jul-14-2016: [email protected] removed extra stuff not needed for stm32 formatted using auto format in arduino ide */ #ifndef RINGBUFFER_T_H_ #define RINGBUFFER_T_H_ #include /** struct_is_power_of_two - power of 2 checker enforce at compile time that SIZE is power of 2 and >= 2 */ template struct is_power_of_two { enum {val = (SIZE >= 2) & (SIZE > 1) & !(SIZE & (SIZE - 1))}; static const unsigned badSIZE[(val == 1) ? 1 : -1]; // SIZE is not a power of 2 if you an error here. }; /** uint16x2_t - a union containing 16 bit head and tail offsets into the ring buffer. The union allows the c code to grab both values with one assembler instruction access. */ union uint16x2_t { // access as 32 bit unsigned long both; // -- or as 2 16 bit values -- struct { unsigned head: 16; unsigned tail: 16; }; }; /** ringbuffer_t - provide a circular_buffer without disabling interrupts expects a power of 2 container, and only one reader and one writer container capacity SIZE-1 */ template < typename T, /* works with any type */ uint16_t SIZE, /* how many elements-1 must be power of 2 */ typename POP_T = int16_t, /* return type of pop_front */ POP_T EMPTY_ELEM = (POP_T) - 1 /* default return value when empty */ > struct ringbuffer_t { // --- private structure data --- // although variables are accessible because this is a struct volatile uint16x2_t offsets; // comes first so we can use 0 offset to variables // for fastest access T elements[SIZE]; enum { CAPACITY = SIZE - 1 }; // leave one slot open is_power_of_two check_buffer_size; // your SIZE is not a power of 2, if you get an error here // --- public methods --- // write access zeros out head and tail inline void clear(void ) { offsets.both = 0; } // return the count of used slots size_t available() { register uint16x2_t temp = { offsets.both }; temp.both = (temp.head - temp.tail) & CAPACITY; return temp.both; } // return maximum number of slots available size_t capacity() { return CAPACITY; } // returns true when there is no used slots bool inline empty() { return !available(); } // returns true when all slots used bool inline full() { return available() == capacity(); } /* push_back() - adds an element to the end of the queue Note: affects head, reads tail, element ignored if overflow ~300 ns @72MHz */ void push_back(const T element) { register uint16_t temp_head = offsets.head; elements[temp_head++] = element; temp_head &= CAPACITY; if ( !(temp_head == offsets.tail) ) { // !full offsets.head = temp_head; } return; } // no bounds check version, affects head ~250 ns @72MHz void push_back_nc(const T element) { register uint16_t temp_head = offsets.head; elements[temp_head++] = element; offsets.head = temp_head & CAPACITY; return; } // affects tail, reads head POP_T pop_front(void) { register uint16x2_t temp = { offsets.both }; if ( (temp.head - temp.tail) & CAPACITY ) { // !empty POP_T elem = elements[temp.tail++]; offsets.tail = temp.tail & CAPACITY; return elem; } return EMPTY_ELEM; // on underflow return default element } // no bounds check, affects tail POP_T pop_front_nc(void) { register uint16_t temp_tail = offsets.tail; POP_T elem = elements[temp_tail++]; offsets.tail = temp_tail & CAPACITY; return elem; } }; #endif /* RINGBUFFER_T_H_ */

Сжимать
Чт 14 июля 2016 г., 8:45 вечера
Спасибо, Рик! Очень хорошо!

Эдогальдо
Чт 14 июля 2016 г., 8:52 вечера
Хей Рик, интересное решение.
Один запрос: возможно ли для вас сравнить также мою версию RING_BUFFER?

В любом случае, один вопрос: я не понял, сказали ли вы, что ваше решение безопасно в гонке даже без необходимости отключения прерываний, и, если да, то почему, не могли бы вы объяснить?


Спасибо, e.

Рик Кимбалл
Чт 14 июля 2016 г., 21:11
Эдогальдо написал:В любом случае, один вопрос: я не понял, сказали ли вы, что ваше решение безопасно в гонке даже без необходимости отключения прерываний, и, если да, то почему, не могли бы вы объяснить?

Рик Кимбалл
Чт 14 июля 2016 г., 21:21
Эдогальдо написал:Один запрос: возможно ли для вас сравнить также мою версию RING_BUFFER?

Рик Кимбалл
Солнце 12 марта 2017 г., 19:27
Я переходил к руке-нищскому-eabi-gcc 6.2.1. Я смотрел на свой выход ASM и заметил, что компилятор становится еще умнее.
В оригинальном коде, который я опубликовал, функция доступного () использовалась 3 регистра, R0, R2, R3. В 6.2.1 сгенерированный код, он выполнен с одним регистром и одним вызовом ASM:

ARM-None-Eabi-GCC 4.8.3 // return the count of used slots size_t available() { register uint16x2_t temp = { offsets.both }; 800015a: 6803 ldr r3, [r0, #0] temp.both = (temp.head - temp.tail) & CAPACITY; 800015c: b29a uxth r2, r3 800015e: eba2 4013 sub.w r0, r2, r3, lsr #16 return temp.both; } 8000162: f000 000f and.w r0, r0, #15 8000166: 4770 bx lr

C_D
Ср. 5 июля 2017 г., 21:20
Я действительно заинтересован в том, чтобы использовать что -то подобное в одном из моих проектов, в настоящее время у меня есть несколько буферов кольца, хранящих значения датчиков и расчет перемещающих средних. Подобный класс шаблонов будет отлично подходит для привыкания моего эскиза.

У меня быстрый вопрос, что произойдет, если вы попытаетесь использовать его с массивом? Из -за некоторого чтения я вижу, что классы шаблонов работают с массивами в качестве дата, но я предполагаю, что мне придется написать специальные функции для копирования и возврата данных?

РЕДАКТИРОВАТЬ:
Оглядываясь назад, проходить целые массивы в кольцевой буфер - это странная вещь, чтобы хотеть делать. В конце концов, мое решение было добавить метод discard_front (), который мог бы удалить неверные сообщения с передней части очереди.

C_D
Ср. 04 октября 2017 г. 2:26
Только что вернулся к моему проекту, используя эти аккуратные кольцевые бревни, и обнаружил, что в опубликованном коде есть немного ошибки.
typename POP_T = int16_t, /* return type of pop_front */ ... // affects tail, reads head POP_T pop_front(void) { register uint16x2_t temp = { offsets.both }; if ( (temp.head - temp.tail) & CAPACITY ) { // !empty POP_T elem = elements[temp.tail++]; offsets.tail = temp.tail & CAPACITY; return elem; } return EMPTY_ELEM; // on underflow return default element }

Рик Кимбалл
Ср. 04 октября 2017 г. 3:54
Вам просто нужно предоставить тип данных POP_T и сделать его таким же, как тип данных T в определении шаблона.

RINGBUFFER_T<T, buffer_size, t, -1> ценности; /* <<<< Обратите внимание, что это отличается от вашего */

Для POP_T вы можете использовать тот же тип, что и для элемента T или другого.

Я использовал эту «функцию» в свою пользу, используя Uint8_T в качестве типа данных элемента, но int для POP_T с пустым значением -1.

Это позволяет мне зацикливаться на возвратном значении: ringbuffer_t buffer; ... int c; while((c=buffer.pop_front()) != -1) { Serial.print((char)c); }

Mrburnette
Ср. 04 октября 2017 г. 13:44
[Рик Кимбалл - Чт 14 июля 2016 г., 17:21] - ...
Примечание. Поскольку этот материал основан на шаблонах, вы можете изменить буфер элементов из массива данных UINT8_T на что -то совершенно другое. Это может быть кольцевой буфер пользовательской структуры, кольцевой буфер int16_t или даже кольцевой буфер с указателями функций.
...
Рик,
Ух ты ... серьезно круто. Мы, смертные, недостойны, но мы все равно будем использовать его!

Луча

C_D
Ср. 04 октября 2017 г. 11:54
[Рик Кимбалл - Ср. 04 октября 2017 г. 3:54] - Вам просто нужно предоставить тип данных POP_T и сделать его таким же, как и тип данных t.

Я использовал эту «функцию» в свою пользу, используя UINT8_T в качестве типа данных, но вернул int16_T с пустым значением -1.
Ага, я знал, что для этого должна быть причина, я просто не мог придумать ни одной причины, по которой тип возврата будет отличаться от сохраненного типа. Это имеет больше смысла сейчас!
[Рик Кимбалл - Ср. 04 октября 2017 г. 3:54] - Кстати: вы, вероятно, должны позвонить значениям.clear () в вашем конструкторе, чтобы он будет работать в качестве локальной переменной в стеке.
Это просто обеспечение того, чтобы структура была инициализирована до 0, когда она создается?

Кроме того, почему емкость меньше фактического размера буфера? Это действительно замедляет мой код движения, если количество фактических элементов не является силой двух, потому что вам нужно сделать фактическое разделение, а не просто сдвиг.

Рик Кимбалл
Чт 05 октября 2017 г. 15:45
[C_D - Ср. 04 октября 2017 г. 11:54 - Это просто обеспечение того, чтобы структура была инициализирована до 0, когда она создается?
Да. Однако это только проблема, если она инстановилась в стеке. Если вы сделаете это глобальной переменной .Секция BSS обнуляется при запуске.
[C_D - Ср. 04 октября 2017 г. 11:54 - Кроме того, почему емкость меньше фактического размера буфера?
Я оставляю один слот открытым, чтобы обнаружить, когда буфер элемента заполнен без необходимости подсчета. Существуют разные способы реализации круглого буфера. Я использовал подход, который использует 2 индекса и буфер элемента. Это минимизирует размер кода и проблемы с блокировкой.

Этот код делает несколько предположений:

1. Там будет один производитель и один потребитель. Как правило, производитель - это прерывание, получающая асинхронные данные и основной поток, действуя как потребитель. Я изначально создал его для работы с данными UART.

2. Я использовал данные dataType uint16_t для индексов головы и хвоста. Это ограничивает размер буферных элементов (1<<16) -1 (65535). Это не проблема на чипсах, на которые я нацелен. STM32F103C8 имеет всего 20 тыс. ОЗУ. Тем не менее, это также позволяет мне прочитать атомное чтение обоих индексов, не блокируя, поскольку я вкладываю два значения uint16_t в Uint32_t Union. Производитель - единственный, кто собирается установить индекс головы. Потребитель - единственный, кто собирается установить индекс хвоста. Это делает код довольно безопасным, если вы игнорируете случай, когда он переполнен. Я оставляю это в качестве упражнения читателю. Они будут знать лучше о том, как они производят и потребляют. Они могут решить заблокировать или игнорировать в условиях переполнения. Я не помещаю это в код.

3. Я предполагаю, что размер буферного элемента, который является мощностью 2, поэтому я могу использовать маску для работы с обертыванием буферного индекса вместо использования оператора модуля. Смотрите мой предыдущий пост Это показывает, как Arm-None-Eabi-GCC уменьшает вычисление количества доступных элементов до одной инструкции Thumb2.
[C_D - Ср. 04 октября 2017 г. 11:54 - Это действительно замедляет мой код движения, если количество фактических элементов не является силой двух, потому что вам нужно сделать фактическое разделение, а не просто сдвиг.
Вы уже проверяете, заполняется ли буфер, чтобы решить, нужно ли вам выбросить элемент. Вместо этого вы всегда можете использовать размер 16 для кольцевого буфера, а затем проверить наличие подсчета () 8 вместо буфера.емкость(). Это позволило бы вам использовать распределение смены. Тем не менее, вам все равно приходится иметь дело с начальными условиями, когда у вас меньше 8 образцов, используя обычную инструкцию по разделению.

C_D
Чт, 5 октября 2017 г. 20:39
[Рик Кимбалл - Чт, 05 октября 2017 г. 15:45] - вы уже проверяете, заполняется ли буфер, чтобы решить, нужно ли вам выбрасывать элемент. Вместо этого вы всегда можете использовать размер 16 для кольцевого буфера, а затем проверить наличие подсчета () 8 вместо буфера.емкость(). Это позволило бы вам использовать распределение смены. Тем не менее, вам все равно приходится иметь дело с начальными условиями, когда у вас меньше 8 образцов, используя обычную инструкцию по разделению.
Это на самом деле не плохая идея, торговля от скорости на небольшое количество потраченного впустую пространства. Для моего нынешнего приложения я отбираю относительно медленно, чтобы целочисленное разделение не проблема, но это, вероятно, лучший подход, если мне нужно ускорить ситуацию дальше по трассе, спасибо 8-)

Супер простой RTC

СПИ-3 провода