С++ или «снова об умении готовить»
- Войдите на сайт для отправки комментариев
Коллеги, сильно меня зацепила вот эта тема, а тут, вдруг случайно родилось интересное (для меня, по крайней мере) её развитие, коим и спешу с Вами поделиться.
Думаю, не одного меня достало нытьё про то, что 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 в четырёх случаях (оба параметра константы, один константа, а другой переменная и оба - переменные).
Скетч
#include <Printing.h> #include <DigitalPins.h> #include <PinOps.h> volatile int value, pin; volatile bool b; static const long repeats = 100000L; #define TEST_ROUNTINE(tech, pm, dr, dw, _pin, _val) \ uint32_t test##tech (void) { \ const uint32_t startTime = millis(); \ for (long i = 0; i < repeats; ++i) { \ pm(_pin, _val);pm(_pin, _val);pm(_pin, _val);pm(_pin, _val); \ pm(_pin, _val);pm(_pin, _val);pm(_pin, _val);pm(_pin, _val); \ b = dr(3);b = dr(3);b = dr(3);b = dr(3); \ b = dr(3);b = dr(3);b = dr(3);b = dr(3); \ dw(3, HIGH);dw(3, HIGH);dw(3, HIGH);dw(3, HIGH); \ dw(3, HIGH);dw(3, HIGH);dw(3, HIGH);dw(3, HIGH); \ } \ return millis() - startTime; \ } #define TEST_PINOPS(qv, _pin, _val) TEST_ROUNTINE(PinOps##qv, PinOps::pinMode, PinOps::digitalRead, PinOps::digitalWrite, _pin, _val) #define TEST_DIDITALPINS(qv, _pin, _val) TEST_ROUNTINE(DigitalPins##qv, _pinMode, _digitalRead, _digitalWrite, _pin, _val) #define TEST_WIRING(qv, _pin, _val) TEST_ROUNTINE(Wiring##qv, pinMode, digitalRead, digitalWrite, _pin, _val) TEST_PINOPS(_cc, 3, 1) TEST_PINOPS(_cv, 3, value) TEST_PINOPS(_vc, pin, 1) TEST_PINOPS(_vv, pin, value) TEST_DIDITALPINS(_cc, 3, 1) TEST_DIDITALPINS(_cv, 3, value) TEST_DIDITALPINS(_vc, pin, 1) TEST_DIDITALPINS(_vv, pin, value) TEST_WIRING(_cc, 3, 1) TEST_WIRING(_cv, 3, value) TEST_WIRING(_vc, pin, 1) TEST_WIRING(_vv, pin, value) void setup() { value = analogRead(0) % 2; pin = (analogRead(0) % 18) + 2; Serial.begin(57600); printf("%12s Wiring DigitalPins PinOps\r\n", ""); printf("%-12s %6lu %11lu %6lu\r\n", "const,const", testWiring_cc(), testDigitalPins_cc(), testPinOps_cc()); printf("%-12s %6lu %11lu %6lu\r\n", "const,var", testWiring_cv(), testDigitalPins_cv(), testPinOps_cv()); printf("%-12s %6lu %11lu %6lu\r\n", "var,const", testWiring_vc(), testDigitalPins_vc(), testPinOps_vc()); printf("%-12s %6lu %11lu %6lu\r\n", "var,var", testWiring_vv(), testDigitalPins_vv(), testPinOps_vv()); } void loop(void) {}Результат из монитора порта (в таблице печатается время в миллисекундах стотысячекратного выполнения последовательности из 8-ми pinMode, 8-ми digitalRead и 8-ми digitalWrite - см. скетч)
Wiring DigitalPins PinOps const,const 11371 547 547 const,var 11823 949 900 var,const 11522 2710 1956 var,var 11973 3270 2711Как видим, для констант DigitalPins и PinOps одинаковы (оба пишут напрямую в порты), а для переменных, PinOps выигрывает также, как он выигрывал по памяти.
Спасибо за замечание.
И мы могли бы вести войну
Против тех, кто против нас,
Так как те, кто против тех, кто против нас,
Не справляются с ними без нас.