С++ или «снова об умении готовить»
- Войдите на сайт для отправки комментариев
Коллеги, сильно меня зацепила вот эта тема, а тут, вдруг случайно родилось интересное (для меня, по крайней мере) её развитие, коим и спешу с Вами поделиться.
Думаю, не одного меня достало нытьё про то, что digitalRead/digitaWrite/pinMode ну такие, просто, ужасно медленные. Ноют чаще всего те, для кого проблема медленности этих функций стоит примерно на 100501-ом месте в списке их бед, начинающемся с "памагити кампилятор ругаеццо", но ноют.
Известно, что если пины и их значения - константы, то можно использовать CyberLib. Но, во-первых, у неё непривычный синтаксис, а во-вторых, если вдруг в скетче на 100500 констант пришлось таки поработать с одной переменной, то придётся использовать и обычный digitalRead - т.е. мешать в одном коде два синтаксиса работы с портами. Решением может стать библиотека DigitalPins. Она использует привычный синтаксис и вполне работает в том же синтаксисе и с переменными (правда, при этом digitalWrite уже не компилируется в одну команду, чудес не бывает). Но, Вы видели её код? Та ещё помойка!
Но, до некоторых пор, я считал DigitalPins приличным решением, пригодным и для констант, и для переменных. Но, лишь до тех пор, пока не подумал, а почему бы собственно, не написать работу с пинами просто на С++, безо всяких препроцессоров, в лоб? Может, результаты будут не хуже?
Итак, вот простая (всего 38 строчек) непосредственная реализация digitalRead/digitalWrite/pinMode для UNO-подобных. Все три функции являются статическими методами класса PinOps и вызывать их надо с соответсвующим префиксом (например, PinOps::digitalRead(10); ).
#ifndef PINOPS_H #define PINOPS_H struct PinOps { static inline volatile uint8_t & getIn(const uint8_t pin) __attribute__((always_inline)) { return (pin < 8) ? PIND : (pin < 14) ? PINB : PINC; // может ошибку обработать? } static inline volatile uint8_t & getOut(const uint8_t pin) __attribute__((always_inline)) { return (pin < 8) ? PORTD : (pin < 14) ? PORTB : PORTC; // может ошибку обработать? } static inline volatile uint8_t & getDDR(const uint8_t pin) __attribute__((always_inline)) { return (pin < 8) ? DDRD : (pin < 14) ? DDRB : DDRC; // может ошибку обработать? } static inline uint8_t getBitMask(const uint8_t pin) __attribute__((always_inline)) { return 1 << ((pin < 8) ? pin : (pin < 14) ? (pin - 8) : (pin - 14)); // может ошибку обработать? } static inline void digitalWrite(const int8_t pin, const bool val) __attribute__((always_inline)) { if (val) getOut(pin) |= getBitMask(pin); else getOut(pin) &= ~getBitMask(pin); } static inline bool digitalRead(const int8_t pin) __attribute__((always_inline)) { return static_cast<bool>(getIn(pin) & getBitMask(pin)); } static inline void pinMode(const int8_t pin, const bool val) __attribute__((always_inline)) { if (val) getDDR(pin) |= getBitMask(pin); else getDDR(pin) &= ~getBitMask(pin); } }; #endif // PINOPS_H
Все эксперименты проводились на IDE 1.8.9 с опциями из коробки, плата – Uno. Для экспериментов были написаны четыре скетча на все возможные случаи. Скетчи такие:
- Все параметры всех функций – константы;
- Номера пинов – константы, а значения – переменные;
- Номера пинов – переменные, а значения – константы;
- Все параметры – переменные.
Каждый из скетчей позволяет запуститься в режиме оригинальных ардуиновских функций, в режиме использования DigitalPins и в режиме использования PinOps. Первый скетч (где все константы) ещё и в режиме прямой работы с портами (к переменным это неприменимо). Для конфигурации скетча (с какой библиотекой работать) нужно задать константу METHOD (строка №6) одним из значений из строк №№1-4.
Вот скетчи и результаты экспериментов:
Эксперимент №1. Все параметры всех функций – константы.
#define DIRECT_PORTS 1 #define ORIGINAL_ARDUINO 2 #define DIGITALPINS 3 #define PINOPS 4 #define METHOD ORIGINAL_ARDUINO ////////////////////////////////////// #if METHOD == DIGITALPINS #include <DigitalPins.h> #endif #if METHOD == PINOPS #include <PinOps.h> #endif void setup() { #if METHOD == PINOPS PinOps::pinMode(3, OUTPUT); const bool b = PinOps::digitalRead(3); PinOps::digitalWrite(3, HIGH); #elif METHOD == DIGITALPINS _pinMode(3, OUTPUT); const bool b = _digitalRead(3); _digitalWrite(3, HIGH); #elif METHOD == DIRECT_PORTS DDRD |= 8; const bool b = static_cast<bool>(PIND & 8); PORTD |= 8; #elif METHOD == ORIGINAL_ARDUINO pinMode(3, OUTPUT); const bool b = digitalRead(3); digitalWrite(3, HIGH); #endif PORTB = b; // просто, чтобы не вязалась, что b не используется. } void loop(void) {}
Метод | Память программ | Память данных |
Прямая работа с портами | 458 | 9 |
DigitalPins | 458 | 9 |
PinOps | 458 | 9 |
Оригинальный Ардуино | 788 | 9 |
Ничего неожиданного. И DigitalPins, и PinOps показали результат прямой работы с портами, т.е. именно в неё и скомпилировались.
Эксперимент №2. Параметр пин – константа, а параметр значение – переменная.
#define ORIGINAL_ARDUINO 2 #define DIGITALPINS 3 #define PINOPS 4 #define METHOD ORIGINAL_ARDUINO ////////////////////////// #if METHOD == DIGITALPINS #include <DigitalPins.h> #endif #if METHOD == PINOPS #include <PinOps.h> #endif void setup() { const int v = analogRead(0); #if METHOD == PINOPS PinOps::pinMode(3, v); const bool b = PinOps::digitalRead(3); PinOps::digitalWrite(3, v); #elif METHOD == ORIGINAL_ARDUINO pinMode(3, v); const bool b = digitalRead(3); digitalWrite(3, v); #elif METHOD == DIGITALPINS _pinMode(3, v); const bool b = _digitalRead(3); _digitalWrite(3, v); #endif PORTB = b; // просто, чтобы не вязалась, что b не используется. } void loop(void) {}
Метод | Память программ | Память данных |
DigitalPins | 540 | 9 |
PinOps | 510 | 9 |
Оригинальный Ардуино | 882 | 9 |
А вот здесь PinOps вырывается немного вперёд, оптимизатор делает своё дело!
Эксперимент №3. Параметр пин – переменная, а значение – константа.
#define ORIGINAL_ARDUINO 2 #define DIGITALPINS 3 #define PINOPS 4 #define METHOD PINOPS ////////////////////////////// #if METHOD == DIGITALPINS #include <DigitalPins.h> #endif #if METHOD == PINOPS #include <PinOps.h> #endif void setup() { const int p = analogRead(0); #if METHOD == PINOPS PinOps::pinMode(p, OUTPUT); const bool b = PinOps::digitalRead(p); PinOps::digitalWrite(p, HIGH); #elif METHOD == ORIGINAL_ARDUINO pinMode(p, OUTPUT); const bool b = digitalRead(p); digitalWrite(p, HIGH); #elif METHOD == DIGITALPINS _pinMode(p, OUTPUT); const bool b = _digitalRead(p); _digitalWrite(p, HIGH); #endif PORTB = b; // просто, чтобы не вязалась, что b не используется. } void loop(void) {}
Метод | Память программ | Память данных |
DigitalPins | 1056 | 9 |
PinOps | 704 | 9 |
Оригинальный Ардуино | 828 | 9 |
Опа! А при переменных номерах пинов DigitalPins проигрывает оригинальным ардуиновским функциям из Wiring! Круто! PinOps, по-прежнему, лидер.
Эксперимент №4. Все параметры всех функций - переменные.
#define ORIGINAL_ARDUINO 2 #define DIGITALPINS 3 #define PINOPS 4 #define METHOD ORIGINAL_ARDUINO ////////////////////////////// #if METHOD == DIGITALPINS #include <DigitalPins.h> #endif #if METHOD == PINOPS #include <PinOps.h> #endif void setup() { const int v = analogRead(0); const int p = analogRead(0); #if METHOD == PINOPS PinOps::pinMode(p, v); const bool b = PinOps::digitalRead(p); PinOps::digitalWrite(p, v); #elif METHOD == ORIGINAL_ARDUINO pinMode(p, v); const bool b = digitalRead(p); digitalWrite(p, v); #elif METHOD == DIGITALPINS _pinMode(p, v); const bool b = _digitalRead(p); _digitalWrite(p, v); #endif PORTB = b; // просто, чтобы не вязалась, что b не используется. } void loop(void) {}
Метод | Память программ | Память данных |
DigitalPins | 1940 | 9 |
PinOps | 884 | 9 |
Оригинальный Ардуино | 904 | 9 |
Ну, тут уже совсем неприлично! DigitalPins проиграл Wiring более, чем в два раза! И эту библиотеку я считал хорошей!? А PinOps – опять лидер!
Вот, собственно и вся недолга. Никаких извращений, простое и понятное написание программы на С++ безо всяких извратов с препроцессором и даже без constexpr. Этот язык многое позволяет, просто «готовить нужно уметь»!
Маленькое замечание: началось все с того, что:
А закончилось статистикой по используемой памяти.
А закончилось статистикой по используемой памяти.
Это память кода. То, что при константах она "байт в байт" означает, что при константах эта "библиотека" генерирует прямую запись в порты. Разумеется, я это проверял и глядя на листинг. Ну, а при переменных - опять же кода меньше ....
Считаете. что для полноты и законченности нужно и скорость замерить? Могу сделать.
Да и начиналось с "традиционного плача", а он и про скорость и про память - про всё плачут :)
Дело не в полноте и законченности, а в том, что полученный "ответ" оказался совсем не на тот "вопрос", что был сформулирован в начале.
Евгений, Вам же наверняка приходилось писать рецензии, отзывы и т.п. Неужели Вас не коробит, когда в постановке задачи указывается одно, а исследуется совершенно другое?
Отстань ты от юродивого, он популярные дефайники в инлайны перевел и тихо радуется что С++. Ему и не вдомек что оно только для 328-го.
Ну зачем же так грубо?
Я, например, с интересом читаю все материалы Евгения. И в данном случае тоже прочитал с интересом и даже получил полезную для себя информацию, несмотря на имеющиеся, на мой взгляд, недостатки.
Ну а по поводу "только для 328", так с одной стороны, претензий на что-то иное и не было, а с другой - Евгений обращает внимание не на специфичные конкретно для 328 хаки, а на достаточно общие тенденции, иллюстрируя их на примере конкретного "камня".
Что касается конкретно автора, то у него бывают посты, которые просто хочется распечатать, повесить на стенку и любоваться. А бывают менее удачные. Я, как читатель, заинтересован в том, чтобы более удачных было больше. Если мои советы в чем-то смогут этому способствовать - буду рад. Если нет, значит, не судьба.
Ну а насет популярности... Вот я, напрмер, счиаю себя опытным программистом, но при этом С/С++ - не мои родные языки, и я пишу, используя в основном их подмножество, пересекающееся с возможностями других языков. Ну т.е. все императивные языки имеют некоторое общее подмножество, которое в принципе достаточно для написания любых программ, но при этом каждый из них имеет что-то свое, что позволяет писать программы на этом языке проще и эффективнее. Поэтому зачастую даже описание использования стандартных для С++ возможностей мне интересно. И, думаю, более чем для половины участников форума - тоже.
Ну, между "заходом" и "исследованием" действительно разрыв, начни ты заход с памяти, а не с медленности, всё было бы цельно ...
А по сути, я как-то пробовал, но не классом, а отдельными функциями - хрен там, не получилось. Простого inline не хватило, а атрибут always_inline к отдельным функциям неприменим. Ты же их в класс упаковал и тогда уж применил атрибут - в общем, на тоненького между двумя огнями проскочил. Ну, собственно это и есть "готовка" - "там чуток специй, тут чуть настояться дать ..." Сейчас тебе начнут говорить: "вынеси из класса, а то я сам вынесу, не хнычь тогда и готовь булки", есть у нас такие спецы.
Замечание правильное, не знаю, как так вышло. Исправляя недоработку, привожу протокол испытаний на скорость. Сравнивались Wiring, DigitalPins и PinOps в четырёх случаях (оба параметра константы, один константа, а другой переменная и оба - переменные).
Скетч
Результат из монитора порта (в таблице печатается время в миллисекундах стотысячекратного выполнения последовательности из 8-ми pinMode, 8-ми digitalRead и 8-ми digitalWrite - см. скетч)
Как видим, для констант DigitalPins и PinOps одинаковы (оба пишут напрямую в порты), а для переменных, PinOps выигрывает также, как он выигрывал по памяти.
Спасибо за замечание.
И мы могли бы вести войну
Против тех, кто против нас,
Так как те, кто против тех, кто против нас,
Не справляются с ними без нас.