С++ или «снова об умении готовить»

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Коллеги, сильно меня зацепила вот эта тема, а тут, вдруг случайно родилось интересное (для меня, по крайней мере) её развитие, коим и спешу с Вами поделиться.

Думаю, не одного меня достало нытьё про то, что 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. Для экспериментов были написаны четыре скетча на все возможные случаи. Скетчи такие:

  1. Все параметры всех функций – константы;
  2. Номера пинов – константы, а значения – переменные;
  3. Номера пинов – переменные, а значения – константы;
  4. Все параметры – переменные.

Каждый из скетчей позволяет запуститься в режиме оригинальных ардуиновских функций, в режиме использования 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. Этот язык многое позволяет, просто «готовить нужно уметь»!

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Маленькое замечание: началось все с того, что:

Цитата:
digitalRead/digitaWrite/pinMode ну такие, просто, ужасно медленные

А закончилось статистикой по используемой памяти.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

andriano пишет:

А закончилось статистикой по используемой памяти.

Это память кода. То, что при константах она "байт в байт" означает, что при константах эта "библиотека" генерирует прямую запись в порты. Разумеется, я это проверял и глядя на листинг. Ну, а при переменных - опять же кода меньше ....

Считаете. что для полноты и законченности нужно и скорость замерить? Могу сделать.

Да и начиналось с "традиционного плача", а он и про скорость и про память - про всё плачут :)

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Дело не в полноте и законченности, а в том, что полученный "ответ" оказался совсем не на тот "вопрос", что был сформулирован в начале.

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

Logik
Offline
Зарегистрирован: 05.08.2014

Отстань ты от юродивого, он популярные дефайники в инлайны перевел и тихо радуется что С++. Ему и не вдомек что оно только для 328-го.

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

Ну зачем же так грубо?

Я, например, с интересом читаю все материалы Евгения. И в данном случае тоже прочитал с интересом и даже получил полезную для себя информацию, несмотря на имеющиеся, на мой взгляд, недостатки.

Ну а по поводу "только для 328", так с одной стороны, претензий на что-то иное и не было, а с другой - Евгений обращает внимание не на специфичные конкретно для 328 хаки, а на достаточно общие тенденции, иллюстрируя их на примере конкретного "камня".

Что касается конкретно автора, то у него бывают посты, которые просто хочется распечатать, повесить на стенку и любоваться. А бывают менее удачные. Я, как читатель, заинтересован в том, чтобы более удачных было больше. Если мои советы в чем-то смогут этому способствовать - буду рад. Если нет, значит, не судьба.

Ну а насет популярности... Вот я, напрмер, счиаю себя опытным программистом, но при этом С/С++ - не мои родные языки, и я пишу, используя в основном их подмножество, пересекающееся с возможностями других языков. Ну т.е. все императивные языки имеют некоторое общее подмножество, которое в принципе достаточно для написания любых программ, но при этом каждый из них имеет что-то свое, что позволяет писать программы на этом языке проще и эффективнее. Поэтому зачастую даже описание использования стандартных для С++ возможностей мне интересно. И, думаю, более чем для половины участников форума - тоже.

Ворота
Ворота аватар
Offline
Зарегистрирован: 10.01.2016

Ну, между "заходом" и "исследованием" действительно разрыв, начни ты заход с памяти, а не с медленности, всё было бы цельно ...

А по сути, я как-то пробовал, но не классом, а отдельными функциями - хрен там, не получилось. Простого inline не хватило, а атрибут always_inline к отдельным функциям неприменим. Ты же их в класс упаковал и тогда уж применил атрибут - в общем, на тоненького между двумя огнями проскочил. Ну, собственно это и есть "готовка" - "там чуток специй, тут чуть настояться дать ..." Сейчас тебе начнут говорить: "вынеси из класса, а то я сам вынесу, не хнычь тогда и готовь булки", есть у нас такие спецы.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Замечание правильное, не знаю, как так вышло. Исправляя недоработку, привожу протокол испытаний на скорость. Сравнивались 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 выигрывает также, как он выигрывал по памяти.

Спасибо за замечание.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

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

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

ЕвгенийП пишет:
Срача здесь я не допущу "полицейскими мерами", а говорить ... мы свободные люди в свободной стране и каждый волен демонстрировать свою глупость когда и как пожелает.

И мы могли бы вести войну

Против тех, кто против нас,

Так как те, кто против тех, кто против нас,

Не справляются с ними без нас.