Этюды для начинающих: Память 1. Что и как не надо делать

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

Колллеги, прошлый этюд вызвал бурный и долго не стихающий аплодисм холивар, счётчик сообщений в котором уже перевалил за тип byte и останавливаться не собирается. Поскольку этюды пишутся для начинающих, я хочу ответить на незаданный вопрос: кому верить, когда толпа гуру вцепилась друг другу в глотки и с брызгами слюны доказывает что-то непонятное на непонятном языке? Ответ простой - никому. Верить можно (если хочется) в Бога, а всё остальное должно подвергаться самостоятельной перепроверке и переосмыслению. Включайте голову, думайте. Где надо - пишите проверочные скетчи и запускайте и делайте собственные выводы.

Я давно собирался написать этюд о памяти и работе с нею, но всё как-то не получалось. Тема настолько глобальна. что требует слишком уж много букв. Поэтому, я решил делать это по частям - сегодня мы посмотрим на общую организацию памяти и разберём наиболее часто встречающиеся ошибки в работе с нею. А там видно будет.

Речь идёт исключительно об ОЗУ - мы не касаемся EEPROM и памяти программы (по крайней мере пока).

Итак, если мы посмотрим в Atmel'овскую документацию, то увидим. что общее распределение памяти у нас примерно таково:

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

Итак, пойдёмте слева направо.

Первой идёт секция .data – это то место, где живут Ваши глобальные (описанные вне всякой функции) и статические (описанные со словом static) переменные.

Далее следует секция .bss – это тоже секция для глобальных и статических переменных, которые (в отличие от жителей секции .data) не нужно инициализировать.

Далее идёт т.н. куча – это место, откуда Вам будет выделяться память, если Вы запросите её при  помощи оператора new или функции malloc (calloc, realloc и т.п.). Куча начинается сразу за секцией .bss и растёт в сторону увеличения адресов.

Коричневая область справа – стек. Стек начинается от конца памяти, и растёт в сторону уменьшения адресов – навстречу куче. На стеке живут переменные, объявленные внутри функций без слова static, а также параметры, передаваемые функциям при вызове, возвращаемые значения и адреса возврата после выполнения функции.

Между стеком и кучей должна всегда оставаться некоторый промежуток (на рисунке он показан с восклицательным знаком). Он гарантирует, что данные стека не налезут на кучу (и наоборот) и не попортят друг друга.

При работе malloc она всегда оставляет до стека не менее, чем __malloc_margin байтов. Если нет такой возможности, она просто откажется выделять память и вернёт ошибку. Позднее, мы ещё поговорим о логике работы malloc.

А вот со стеком всё немного не так радостно. Если куча гарантированно никогда не налезет на стек (Вам просто malloc откажется выделять память), то стек налезть на кучу может запросто. При этом он попортит находящиеся там переменные. А программа скорее всего попортит его самого. Результат трудно предсказуем – обычно после неких необъяснимых конвульсий контроллер уходит на перезагрузку.

Теперь, когда мы знаем как организована память, давайте посмотрим как мы можем её использовать.

В распоряжении программиста (на С++) имеются следующие возможности выделения памяти:

Статические и глобальные переменные

Первые – это те, что описываются со словом static, а вторые – те, что описаны вне всякой функции. Располагаются и те и другие, как уже было сказано в секции .data ещё до начала выполнения программы. Занятую этими переменными память в программе не нужно явно запрашивать и нет никакой возможности освободить – они живут в течение всего времени выполнения программы.

Отличаются друг от друга глобальные и статические переменные областью видимости. Глобальная переменная «видима» от места её описания и до конца файла (если Вы попытаетесь обратиться к ней выше её описания – это ошибка). Статическая (описанная в блоке со словом static) переменная видима от места описания и до конца блока, в котором описана. В разных блоках можно описывать статические переменные с одним и тем же именем и это будут разные переменные – видимы они только внутри своего блока.

Наконец, можно описать глобальную переменную со словом static (т.е. она будет и глобальной, и статической). Про такие переменные я говорить не буду, т.к. они реально нужны только в больших «многофайловых» проектах, а те, кто пишут такие проекты не являются целевой аудиторией моих этюдов. Отмечу только, что если случайно опишете – ничего страшного не случится – в проекте из одного файла слово static ничего не меняет в описании глобальной переменной.

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

Локальные или автоматические переменные

Это переменные, которые Вы описываете внутри функции/блока без слова static. Память для таких переменных выделяется при входе в функцию на стеке. Освобождается память при выходе из функции (здесь есть тонкость, о которой мы ещё будем говорить позже!). Т.е. после выхода из функции, память локальной переменной освобождена и её может занять совершенно другая переменная. Не надейтесь, что локальная переменная сохранит своё значение между вызовами функции и не пытайтесь, например, вернуть из функции указатель на локальную переменную! Самое подлое здесь, что это может случайно нормально сработать – если никто ещё эту память загадить не успел, но в общем случае, поведение таких программ непредсказуемо.

Ещё раз, локальные переменные живут на стеке! И живут они до момента выхода из функции.

Динамическая память

Кроме описания переменных, память можно и просто динамически запрашивать. Делается это при помощи функций malloc / calloc / realloc и оператора new. Память выделяется в «куче». Запрошенную динамически память необходимо освобождать, когда она больше не нужна. Запоминаем: память глобальных переменных не освобождается никогда, память локальных переменных освобождается автоматически, динамически же запрошенную память необходимо освобождать явно. Для освобождения памяти служит фнкция free() и оператор delete.

В чём разница между new/delete и malloc/free? С точки зрения выделения памяти – ни в чём. Но именно с точки зрения работы с памятью - так-то разница между ними есть и существенная. Давайте посмотрим как реализованы операторы new и delete в горячо любимой системе программирования Открываем файл …\arduino-1.6.5-r2\hardware\arduino\avr\cores\arduino\new.cpp и смотрим:

#include <stdlib.h>

void *operator new(size_t size) {
  return malloc(size);
}

void *operator new[](size_t size) {
  return malloc(size);
}

void operator delete(void * ptr) {
  free(ptr);
}

void operator delete[](void * ptr) {
  free(ptr);
}

Как видите, new просто тупо вызывает malloc, а delete не менее тупо вызывает free (сейчас набегут «прогеры» и скажут, что очевидно, что mallloc лучше. т.к. экономится один вызов функции. Да, экономится. Поехали дальше!)

Для работы с памятью напрямую (например, при динамическом запросе) в языке существует понятие «указатель». Чтобы описать некую переменную как укахзатель на объект некторого типа, надо после типа поставить символ *. Присвоить значение указателю можно, через оператор взятия адреса (&) , присвоить переменной значение на которое указывает указатель можно через оператор разыменования (*). Например,

char c;          // c – переменная типа char
char * p;       // p – переменная типа «указатель на объект типа char»
p = &c;         // теперь указатель p указывает на переменную c
c = ‘A’;          // присвоили значение
char s = *p;  // s получила значение 'A'

Как не надо работать с памятью!

Позже мы вернёмся ко всем эти типам памяти и рассмотрим их подробно – логику работы, подводные камни и т.п. Сейчас я хочу остановиться на типичных ошибках в работе с памятью. Все ошибки работы с памятью крайне неприятны, так как приводят к непредсказуемому и часто невоспроизводимому поведению системы. Запомните, если система ведёт себя при каждом запуске по-разному – непредсказуемо – первое на что смотрим – ошибки работы с памятью.

Таковых на самом деле три:

  1. Использование неинициализированного или неправильно инициализированного указателя;
  2. Выход за границы массива (или переполнение буфера);
  3. Переполнение стека (стек собою «пропарывает» кучу).

Ну, если с первой ошибкой понятно, когда мы ничего разумного указателю не присвоили, он «указывает» «куда попало» и именно туда мы и будем писать наши данные (или читать оттуда), то со второй могут проявляться весьма забавные эффекты. Например, есть совершенно типичная – хрестоматийная ошибка начинающих писать на С/С++. Они почему-то думают, что если описать массив как

int a[3];

то у него будет третий элемент a[3]. Это не так. Элементы данного массива будут: a[0], a[1] и a[2], а вот попытка использовать a[3] – как раз и есть выход за границы массива. Что при такой попытке произойдёт? Ну, если читаем, то скорее всего ничего страшного, просто прочитаем что там в памяти сразу после массива лежит. А вот если пишем – то мы то, что там после массива лежит, запросто поменяем! И нашей программе это наверняка не понравится!

Давайте пример:

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

void setup() {
	Serial.begin(115200);
	//
	//	Создаём константу a - равную псевдослучаному числу от 0 до 15
	//
	srand(analogRead(0));
	const int a = rand() % 16;
	//
	//	Печатаем a для контроля
	//
	Serial << "a=" << a << "\n"; 
	//
	//	Создаём массив и "случайно" выходим за его границы
	//
	int b[2];
	b[2] = 321;
	//
	//	Печатаем адрес нашей константы a и адрес элемента массива 
	//	в который мы впендюрили 321 - оопс! Так они ж равны!
	//
	Serial << "&a=" << (int)&a << " &b[2]=" << (int)&(b[2]) << "\n";
	//
	//	Печатаем значение константы a
	//	Вы чего-то другого ждали?
	//
	Serial << "a=" << a << "\n";
}

void loop() {
}
//
// Результат
// a=10
// &a=2294 &b[2]=2294
// a=321

Как видите, ошибившись в индексе масива, мы изменили константу! В мало-мальски нетривиальной программе такую ошибку устанешь искать, т.к. "изменённая константа" - это последнее на что подумаешь!

При ошибках работы с памятью и не такое бывает, имейте в виду и будьте крайне внимательны.

Вл втором примере, мы не будем выходить за границы массива и вообще как-либо ошибаться. Формально, мы будем делать всё правильно. Просто стек у нас будет расти, пока кучу не перекроет. Вот смотрите:


template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

//
//	Функция получает параметром указатель на конец массива s, выделенного в куче.
//	Сравнивает его с указателем на свою локальную константу m
//	И если стек ещё не добрался до кучи, то вызывает себя ещё раз.
//	Рано или поздно, стек доберётся до кучи и тогда константа m
// пропишется поверх каких-то переменных кучи. Функция пректарит работу
//
void f(const char * fin) {
	const char m = 'Z';
	static const char *pm = &m;
	if (fin <= &m+1) f(fin);
}

void setup() {
	Serial.begin(115200);
	//
	//	Запрашиваем память в куче.
	//
	size_t largeBlockSize = 11; 
	char * s = (char *) malloc(largeBlockSize);
	if (!s) { Serial << "Memory error!\n"; return; }
	//
	//	Заполняем запрошенную память буквой Q
	// и печатаем для контроля
	//
	memset(s, 'Q', largeBlockSize-1);
	s[largeBlockSize-1] = '\0';
	Serial << "String before:" << s << "\n";
	//
	//	Обращаемся к функции, которая ничего криминального не делает, 
	//	просто много раз вызывает саму себя пока стек не переполнится
	//
	f(s + largeBlockSize);
	//
	//	Печатаем нашу старую добрую s
	//	её в общем-то никто не менял, а вот подиш!
	//
	Serial << " String after:" << s << "\n";
	free(s);
}

void loop() {
}

//  Результат
//
//    String before:QQQQQQQQQQ
//    String after:QQQQQZ
//

На самом деле, этот пример у Вас может не заработать, т.к. он зависит от размера кучи, а та от Вашей версии IDE и т.д. Делать, чтобы не зависел я не стал - пример стал бы вдвое больше и содержал бы массу непонятного. Если у Вас не заработает, попробуйте поменять константу 11 в строке 21 на 12, 13 и т.д. при каком-то значениеи в диапазоне 11-21 заработает обязательно.

Ну, и что мы тут видим? Стек "пропахал" кучу и стековая константа m (равная 'Z') залезла в массив. расположенный в куче. Не верите? Поменяйте в строке 11 Z на другую букву и убедитесь, то в результате мы видим именно её.

Итак, вывод. При работе с памятью, необходимо внимательнейшим образом следить за тремя ошибками:

  • Использование неинициализированного или неправильно инициализированного указателя;
  • Выход за границы массива (или переполнение буфера);
  • Переполнение стека (стек собою «пропарывает» кучу).

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

Piskunov
Offline
Зарегистрирован: 13.02.2014

Подписался ))

gregoryl
Offline
Зарегистрирован: 09.09.2013

Хм.... первое, что точно не надо делать начинающим с памятью, это использовать malloc & new, ибо нефиг фигней страдать...

А то так на втором уроке начнем stl прикручивать.... компайлер сожрет конечно и все красиво будет, но нафига козе баян.

malloc & new это прямой путь в ад по линии фрагментации памяти, если вы еще не в курсе что это, то вам понраятся вууду глюки, причем не те тривиальные, что вы описали с переписыванием констант, которые можно в реальности отловить в три счета, а такие что происходят раз в пару дней в зависимости от нагрузки, а грешить вы будете на нестабильное питание или прохой кондер в стабилизаторе:-) но еще смешнее будет когда в 1 случае на 1000, камушек даже не будет рестартовать а тупо будет делать вид что завис родимый насмерть..... :-)

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

gregoryl пишет:

Хм.... первое, что точно не надо делать начинающим с памятью, это использовать malloc & new, ибо нефиг фигней страдать...

Это Ваше мнение и Вы его имеете :)

gregoryl пишет:

malloc & new это прямой путь в ад по линии фрагментации памяти,
 
 
Ну, если без головы, то можно и более простым путём себе приключений нажить.

gregoryl пишет:

если вы еще не в курсе что это, то вам понраятся вууду глюки,... , а грешить вы будете на нестабильное питание или прохой кондер
 
 
Знаете, Вы за меня не беспокойтесь. Я своё отгрешился. И видал я в этой професси многое и многих. Так что, я ценю Вашу заботу, но обо мне беспокоиться не надо :)
Logik
Offline
Зарегистрирован: 05.08.2014

Явно не хватает упоминания типичной ошибки, возврат из функции указателя на локальную переменную. 

Про фрагментацию кучи: её не будет если следовать правилу, порядок освобождения памяти обратный порядку распределения (последний распределенный освобождается первым). 

Полезное

/* Заклинание определения свободного места */
int freeRam () {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

Про недопустимость активной работы с кучей в микроконтроллерах - да, существует такое мнение, аргументированое тем, вопросом "А что делать если не удалось распределить, память кончилась (фрагментировалась)?! " Ошибку мол без ребута не исправить.

 Мое ИМХО - все можно если осторожно. И если есть много модулей не работающих одновременно, то выделять им память на куче очень правильно.

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

Logik пишет:

Явно не хватает упоминания типичной ошибки, возврат из функции указателя на локальную переменную. 

да, как же? Было в описании локальных переменных. А в списке отдельно не было - разновидность испорченного указателя. Но на этой проблеме мы ещё остановимся при подробном разборе локальных переменных.

Logik пишет:

Про фрагментацию кучи: её не будет если следовать правилу, порядок освобождения памяти обратный порядку распределения (последний распределенный освобождается первым). 

Если б это всегда было возможно :(

Logik пишет:

Полезное

/* Заклинание определения свободного места */
int freeRam () {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

Ну, это не совсем свободное место, это расстояние от стека до кучи. При этом куча может быть фрагментирована и в ней внутри могут быть свободные куски гораздо бОльшие, чем получится здесь. И malloc их выдаст, если попросишь. Т.е. если наужно знать сколько памяти можно хапнуть, то надо просмативать список свободных областей и искать наибольшую свободную.

 

gregoryl
Offline
Зарегистрирован: 09.09.2013

ЕвгенийП пишет:

Знаете, Вы за меня не беспокойтесь. Я своё отгрешился. И видал я в этой професси многое и многих. Так что, я ценю Вашу заботу, но обо мне беспокоиться не надо :)

Упс... вечер удался... :-)

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

gregoryl пишет:

Упс... вечер удался... :-)

:)))))

Спокойной ночи! Мне тоже пора баиньки.

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

ЕвгенийП пишет:

да, как же? Было в описании локальных переменных. А в списке отдельно не было - разновидность испорченного указателя. Но на этой проблеме мы ещё остановимся при подробном разборе локальных переменных.

Извините, заметил позже, сообщение уже не редактировалось.

 

ЕвгенийП пишет:
 
Если б это всегда было возможно :(

Это хороший стиль, его можно придерживатся достаточно часто. Вобще часто наблюдается плохое отношение к куче - выделяй память когда хочеш ещё и без контроля успешности, освобождай хоть иногда, это и порождает фрагментацию и утечки памяти. А это как правило создает ошибки краха приложения (для микроконтроллера ребут) плавающие, редковозникающие или чудеса неописуемые, с логикой на первый взгляд не совместимые, потому и утверждают про "ад". Надо просто придерживатся хорошего стиля или хороше продумывать что в куче творится.

ЕвгенийП пишет:

Ну, это не совсем свободное место, это расстояние от стека до кучи. 

Да. Заклинание применяется при появлении странного поведения системы. Если вернет сотню-другую байт, то скорей мало памяти и стек налазит где-то в процессе.

ЕвгенийП пишет:

внутри могут быть свободные куски гораздо бОльшие, чем получится здесь. 

Не могут. фрагментацию надо не допускать. Если её нет - не могут. Если есть, и так плохо, проявится рано или поздно.

ЕвгенийП пишет:

знать сколько памяти можно хапнуть

Так мы же не бюджет пилим. Забирать всю не хороше! Вдруг кому ещё надо. Мы можем и не знать, какой либе понадобится пару байт - а нету.

 

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Перечитал, но так и не понял 2 момента:

1. Это все есть в каждом учебнике по С/С++ как вступление по работе с памятью. Вы весь учебник решили переписать? А, впрочем, ваше право.

2. Кроме озвученных и типовых ошибок из того же учебника - "как не надо делать" ещё будет или "это фсё"? :)

Давайте дополним ваш текст содержимым, относящимся именно к программированию МИКРОКОНТРОЛЛЕРОВ (а не общие места):

1. Секции .data и .bss.

И та и другая - секция размещения глобальных переменных, массивов и статических объектов. Разница в том, что в одну из них помещаются предварительно инициализированные, а во вторую просто объявленные имена. В вашем коде разница выглядит примерно так:

// Это попадет в секцию .data:

int pinLed = 13;
char myText[100500] = "Hello, friend!";

// Это попадет в секцию .bss:

int pinLed;
char myText2[100500];

Казалось бы, второй способ "некомильфо" - прямой соблазн в коде использовать не инициализированное имя переменной. Окей, а "мы его проинициализируем" в setup()!" ... и вот тут как раз наступает та самая рекомендация "так не надо делать". Почему? Оставлю как задачку для самых пытливых, здесь же выскажу только одну рекомендацию:

Делать надо как в первом варианте.

... остальные "как не надо делать", надеюсь вам дорасскажет ТС, которому - респект за этюды и некоторый "рост" качества изложения материала. Не вижу смысла вмешиваться далее. :)

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

Arhat109-2, весьма обяжете, если будете писать компилируемые примеры. Я здесь очень стараюсь, чтобы примеры были не абстрактными, а такими, что можно попробовать запустить.

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Это не пример программы, а только пример способов определения глобалов и их инициализации. Чего тут "компилировать"? Задачка "почему" - исключительно "на сообразительность" и понимание языка. :)

jeka_tm
jeka_tm аватар
Offline
Зарегистрирован: 19.05.2013

ну почему мне постоянно хочется забанить Arhat109-2 после того как прочитаю его посты

у человека просто дар меня раздражать. может во всем виновата разница в возрасте, а она мне кажется полюбому у нас есть, мне 30. Arhat109-2 сколько тебе если это не тайна?

Клапауций 321
Offline
Зарегистрирован: 17.12.2015

jeka_tm пишет:

у человека просто дар меня раздражать. может во всем виновата разница в возрасте

не - это этническое. он бурят.

Andrey12
Andrey12 аватар
Offline
Зарегистрирован: 26.12.2014

Arhat109-2 пишет:

// Это попадет в секцию .data:


int pinLed = 13;
char myText[100500] = "Hello, friend!";

// Это попадет в секцию .bss:

int pinLed;
char myText2[100500];

Делать надо как в первом варианте.

Ну началось, сказал А говори Б. Мне вот интересно, почему?

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Баррон, 1969г кажись .. "Песня, Название песни, содержание песни, имя названия песни.." :)

Дело в том, что ВСЕ константы тоже .. имеют имена, только "скрытые" или внутренние. Компилятор их собирает в отдельную секцию (.data) .. вот они-то и есть то, чем потом(!) инициализируются переменные, путем пересылки блока этой секции в SRAM (ОЗУ) из Flash. Тот самый код в блоках .init() одна из его секций именно это и делает по старту. То есть, первый способ - выделяется место(!) во flash, под хранение констант и его содержимым позже прописывается оперативка, где выделено место под данные.

Казалось бы - "это плохо", мало того, что место занимается в реальности дважды, так ещё и есть скрытый код пересылки всей пачки! На самом деле не всей, а только тех констант, которыми инициализированы переменные. В коде бывает ещё "много разных" .. часть из них может попасть в код команды, что называется "напрямую" (если есть команда и влезет).

А вот теперь второй способ.

Да, он выглядит выигрышнее первого, в той части что нет ни пересылки ни места во флеш. На первый взгляд кажется что это так, но только на первый. Переменные вы все одно БУДЕТЕ инициализировать, а, стало быть строка "hello, friend!" таки попадет в область .data, со своим "тайным названием песни", но уже БЕЗ своей привязки к переменной. И, там где у вас в коде есть инициализация .. произойдет таже самая пересылка данных, но уже не "единым блоком" и быстро, а "вручную".

Казалось бы "да и пофиг", суммарно то на то и выйдет. Не совсем. Далее в дело вступает .. гарвардская организация МК. Константа, как ей и положено, будет лежать изначально во флеш, а вот ваш код требует в заданном месте (часто setup()) перенести её в переменную в ОЗУ .. и?

Задача 2 любопытным: сколько копий константы "создаст" компилятор? (подсказка: секция init() и групповая пересылка начальных данных).

Andrey12
Andrey12 аватар
Offline
Зарегистрирован: 26.12.2014

То есть если я пишу код

int LevelVolume = 10;

void setup()
   {
      // некие условия
      LevelVolume = 2;
   }

void ChangeLevelVolume()
   {
      // некие условия
      LevelVolume = 5;
   }

Это будет более кошерно чем:

int LevelVolume;

void setup()
   {
      // некие условия
      LevelVolume = 2;
   }

void ChangeLevelVolume()
   {
      // некие условия
      LevelVolume = 5;
   }

Я так и не понял почему.

Я еще могу понять про глобальную переменную которая не меняется. То есть например  присваиваю пину имя, в коде программы его не меняю типа int LEDPIN = 13. Тут я еще могу понять. Как вы и написали что все эти значения перенеслись в нужный отдел памяти за один раз блоком, а не дважды когда один раз объявил переменную, а второй раз присвоил ей значение.

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

Andrey-S
Offline
Зарегистрирован: 02.01.2015

Неплохая статья. Правда, уже направленная на новичков, понимающих хоть что-то в Си, а не самоучек вроде меня (скорее даже большинства), которые кроме команд из раздела "ПРОГРАММИРОВАНИЕ" больше нифига не знают. 

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

Andrey12 пишет:

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

Чтобы 100500-пудово исключить самую возможность использования (по ошибке) неинициализированной переменной. Это страховка от ошибки.

Andrey12 пишет:

Я так и не понял почему.

Объяснения Архата типа "инициализировать быстрее, чем не инициализировать" и "одноразовый (вне цикла) выигрыш микросекунды очень важен" с постоянной припиской "догадайтесь почему" не понимает никто, кроме самого Архата. Так что Вы не одиноки.

bwn
Offline
Зарегистрирован: 25.08.2014

ЕвгенийП пишет:

Объяснения Архата типа "инициализировать быстрее, чем не инициализировать" и "одноразовый (вне цикла) выигрыш микросекунды очень важен" с постоянной припиской "догадайтесь почему" не понимает никто, кроме самого Архата. Так что Вы не одиноки.

А как же интрига? Читатель должен быть всегда в напряжении.))))

bwn
Offline
Зарегистрирован: 25.08.2014

jeka_tm пишет:

ну почему мне постоянно хочется забанить Arhat109-2 после того как прочитаю его посты

у человека просто дар меня раздражать. может во всем виновата разница в возрасте, а она мне кажется полюбому у нас есть, мне 30. Arhat109-2 сколько тебе если это не тайна?

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

Piskunov
Offline
Зарегистрирован: 13.02.2014

А можно ли считать иллюстрацией разницы в способах инициализации такие скетчи?

Пример 1:

float MyVar_01 = 1.14;
float MyVar_02 = 2.14;
float MyVar_03 = 3.14;
float MyVar_04 = 4.14;
float MyVar_05 = 5.14;
float MyVar_06 = 6.14;
float MyVar_07 = 7.14;
float MyVar_08 = 8.14;
float MyVar_09 = 9.14;
float MyVar_10 = 0.14;
 
void setup() {}
void loop() {}
/*
Sketch uses 472 bytes (1%) of program storage space. 
Maximum is 32 256 bytes.

Global variables use 9 bytes (0%) of dynamic memory, 
leaving 2 039 bytes for local variables. Maximum is 2 048 bytes.
*/

Пример 2:

float MyVar_01;
float MyVar_02;
float MyVar_03;
float MyVar_04;
float MyVar_05;
float MyVar_06;
float MyVar_07;
float MyVar_08;
float MyVar_09;
float MyVar_10;
 
void setup() {
 MyVar_01 = 1.14;
 MyVar_02 = 2.14;
 MyVar_03 = 3.14;
 MyVar_04 = 4.14;
 MyVar_05 = 5.14;
 MyVar_06 = 6.14;
 MyVar_07 = 7.14;
 MyVar_08 = 8.14;
 MyVar_09 = 9.14;
 MyVar_10 = 0.14;
 }
void loop() {}
/*
Sketch uses 690 bytes (2%) of program storage space. 
Maximum is 32 256 bytes.

Global variables use 49 bytes (2%) of dynamic memory, 
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.
*/

 

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

С т.з. размера скетча - да, можно, несмотря на то что из первого скетча ваши перменные "соптимизированы" компилятором. Воткните в оба примера, в loop() например их суммирование, что их оптимизатор неповыбрасывал как неиспользуемые - будет нагляднее.

С т.з. роста занимаемой памяти - нет, будет "столько же" скорее всего. У меня получалось разместить структурную константу трижды в памяти, по неопытности ещё в июне-июле, когда делал первую версию автоматного программирования, потому и воткнул там PROGMEM. Первый раз во flash как источник инициализации в секции init() - размер программы, второй раз в память как константу и третий раз - в код установки начальных значений в setup() -- оба в памяти.

Piskunov
Offline
Зарегистрирован: 13.02.2014

А теперь разница не столь очевидна, но есть ))

Пример 1:

float MyVar_01 = 1.14;
float MyVar_02 = 2.14;
float MyVar_03 = 3.14;
float MyVar_04 = 4.14;
float MyVar_05 = 5.14;
float MyVar_06 = 6.14;
float MyVar_07 = 7.14;
float MyVar_08 = 8.14;
float MyVar_09 = 9.14;
float MyVar_10 = 0.14;
 
void setup() {}
void loop() {
  MyVar_01 = MyVar_02;
  MyVar_02 = MyVar_03;
  MyVar_03 = MyVar_04;
  MyVar_04 = MyVar_05;
  MyVar_05 = MyVar_06;
  MyVar_06 = MyVar_07;
  MyVar_07 = MyVar_08;
  MyVar_08 = MyVar_09;
  MyVar_09 = MyVar_10;
  MyVar_10 = MyVar_01;
}
/*
Sketch uses 816 bytes (2%) of program storage space. 
Maximum is 32 256 bytes.
Global variables use 49 bytes (2%) of dynamic memory, 
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.
*/

Пример 2:

float MyVar_01;
float MyVar_02;
float MyVar_03;
float MyVar_04;
float MyVar_05;
float MyVar_06;
float MyVar_07;
float MyVar_08;
float MyVar_09;
float MyVar_10;
 
void setup() {
 MyVar_01 = 1.14;
 MyVar_02 = 2.14;
 MyVar_03 = 3.14;
 MyVar_04 = 4.14;
 MyVar_05 = 5.14;
 MyVar_06 = 6.14;
 MyVar_07 = 7.14;
 MyVar_08 = 8.14;
 MyVar_09 = 9.14;
 MyVar_10 = 0.14;
 }
void loop() {
  MyVar_01 = MyVar_02;
  MyVar_02 = MyVar_03;
  MyVar_03 = MyVar_04;
  MyVar_04 = MyVar_05;
  MyVar_05 = MyVar_06;
  MyVar_06 = MyVar_07;
  MyVar_07 = MyVar_08;
  MyVar_08 = MyVar_09;
  MyVar_09 = MyVar_10;
  MyVar_10 = MyVar_01;
  }
/*
Sketch uses 994 bytes (3%) of program storage space. 
Maximum is 32 256 bytes.
Global variables use 49 bytes (2%) of dynamic memory, 
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.
*/

 

Andrey12
Andrey12 аватар
Offline
Зарегистрирован: 26.12.2014

Но если сделать третий пример, о чем я и писал ранее например инициализировать глобальные переменные, а затем в setup() переопределить их в зависимости от каких либо условий, то получается вот что:

float MyVar_01 = 1.14;
float MyVar_02 = 2.14;
float MyVar_03 = 3.14;
float MyVar_04 = 4.14;
float MyVar_05 = 5.14;
float MyVar_06 = 6.14;
float MyVar_07 = 7.14;
float MyVar_08 = 8.14;
float MyVar_09 = 9.14;
float MyVar_10 = 0.14;

void setup() {
  if (MyVar_01 > 1.5)
  {
    MyVar_01 = 9.14;
    MyVar_02 = 8.14;
    MyVar_03 = 7.14;
    MyVar_04 = 6.14;
    MyVar_05 = 5.14;
    MyVar_06 = 4.14;
    MyVar_07 = 3.14;
    MyVar_08 = 2.14;
    MyVar_09 = 1.14;
    MyVar_10 = 0.14;
  }
}
void loop() {}
/*
Sketch uses 866 bytes (2%) of program storage space.
Maximum is 32 256 bytes.

Global variables use 49 bytes (2%) of dynamic memory,
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.

*/

То есть те же 49 байт. Значит инициализация не экономит память, видно только 1 причину инициализировать глобальные переменные это, как сказал ЕвгенийП, исключение ошибки риска использовать неинициализированную глобальную переменную.

Piskunov
Offline
Зарегистрирован: 13.02.2014

Экономится "program storage space", в принципе уже достаточная причина для инициализации при объявлении.

Andrey12
Andrey12 аватар
Offline
Зарегистрирован: 26.12.2014

Piskunov пишет:

Экономится "program storage space", в принципе уже достаточная причина для инициализации при объявлении.

 

Может я что не понимаю, но "program storage space" не экономится. Без инициализации 804 байта при инициализации 866 байт (смотри пост 25)

float MyVar_01;
float MyVar_02;
float MyVar_03;
float MyVar_04;
float MyVar_05;
float MyVar_06;
float MyVar_07;
float MyVar_08;
float MyVar_09;
float MyVar_10;

void setup() {
  if (MyVar_01 > 1.5)
  {
    MyVar_01 = 9.14;
    MyVar_02 = 8.14;
    MyVar_03 = 7.14;
    MyVar_04 = 6.14;
    MyVar_05 = 5.14;
    MyVar_06 = 4.14;
    MyVar_07 = 3.14;
    MyVar_08 = 2.14;
    MyVar_09 = 1.14;
    MyVar_10 = 0.14;
  }
}
void loop() {}
/*
Sketch uses 804 bytes (2%) of program storage space. 
Maximum is 32 256 bytes.

Global variables use 49 bytes (2%) of dynamic memory, 
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.

*/

 

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

Piskunov пишет:

А теперь разница не столь очевидна, но есть ))

Пример 1:

float MyVar_01 = 1.14;
float MyVar_02 = 2.14;
float MyVar_03 = 3.14;
float MyVar_04 = 4.14;
float MyVar_05 = 5.14;
float MyVar_06 = 6.14;
float MyVar_07 = 7.14;
float MyVar_08 = 8.14;
float MyVar_09 = 9.14;
float MyVar_10 = 0.14;
 
void setup() {}
void loop() {
  MyVar_01 = MyVar_02;
  MyVar_02 = MyVar_03;
  MyVar_03 = MyVar_04;
  MyVar_04 = MyVar_05;
  MyVar_05 = MyVar_06;
  MyVar_06 = MyVar_07;
  MyVar_07 = MyVar_08;
  MyVar_08 = MyVar_09;
  MyVar_09 = MyVar_10;
  MyVar_10 = MyVar_01;
}
/*
Sketch uses 816 bytes (2%) of program storage space. 
Maximum is 32 256 bytes.
Global variables use 49 bytes (2%) of dynamic memory, 
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.
*/

Пример 2:

float MyVar_01;
float MyVar_02;
float MyVar_03;
float MyVar_04;
float MyVar_05;
float MyVar_06;
float MyVar_07;
float MyVar_08;
float MyVar_09;
float MyVar_10;
 
void setup() {
 MyVar_01 = 1.14;
 MyVar_02 = 2.14;
 MyVar_03 = 3.14;
 MyVar_04 = 4.14;
 MyVar_05 = 5.14;
 MyVar_06 = 6.14;
 MyVar_07 = 7.14;
 MyVar_08 = 8.14;
 MyVar_09 = 9.14;
 MyVar_10 = 0.14;
 }
void loop() {
  MyVar_01 = MyVar_02;
  MyVar_02 = MyVar_03;
  MyVar_03 = MyVar_04;
  MyVar_04 = MyVar_05;
  MyVar_05 = MyVar_06;
  MyVar_06 = MyVar_07;
  MyVar_07 = MyVar_08;
  MyVar_08 = MyVar_09;
  MyVar_09 = MyVar_10;
  MyVar_10 = MyVar_01;
  }
/*
Sketch uses 994 bytes (3%) of program storage space. 
Maximum is 32 256 bytes.
Global variables use 49 bytes (2%) of dynamic memory, 
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.
*/

 

В каком это она месте есть? В обоих скетчах

Global variables use 49 bytes (2%) of dynamic memory

Всё одинаково  байт в байт.

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

Andrey12, Piskunov, молодцы! Так всегда и делайте. Есть вопрос - написал, попробовал посмотрел, теперь знаешь!

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

Piskunov пишет:

А можно ли считать иллюстрацией разницы в способах инициализации такие скетчи?

В первом скетче компилятор просто выброси Ваши переменные при оптимизации за ненадобностью :)

Piskunov
Offline
Зарегистрирован: 13.02.2014

ЕвгенийП пишет:

В каком это она месте есть? В обоих скетчах

Global variables use 49 bytes (2%) of dynamic memory

Всё одинаково  байт в байт.

Ну где, где всё одинаково-то?

Sketch uses 816 bytes (2%) of program storage space.

Sketch uses 994 bytes (3%) of program storage space.

На десяти переменных уже один процент экономия!

Piskunov
Offline
Зарегистрирован: 13.02.2014

Andrey12 пишет:

Может я что не понимаю, но "program storage space" не экономится. Без инициализации 804 байта при инициализации 866 байт (смотри пост 25)

float MyVar_01;
float MyVar_02;
float MyVar_03;
float MyVar_04;
float MyVar_05;
float MyVar_06;
float MyVar_07;
float MyVar_08;
float MyVar_09;
float MyVar_10;

void setup() {
  if (MyVar_01 > 1.5)
  {
    MyVar_01 = 9.14;
    MyVar_02 = 8.14;
    MyVar_03 = 7.14;
    MyVar_04 = 6.14;
    MyVar_05 = 5.14;
    MyVar_06 = 4.14;
    MyVar_07 = 3.14;
    MyVar_08 = 2.14;
    MyVar_09 = 1.14;
    MyVar_10 = 0.14;
  }
}
void loop() {}
/*
Sketch uses 804 bytes (2%) of program storage space. 
Maximum is 32 256 bytes.

Global variables use 49 bytes (2%) of dynamic memory, 
leaving 1 999 bytes for local variables. Maximum is 2 048 bytes.

*/

 

Не могу с Вами согласиться с ходу. Есть сомнения.

В примере из №25 Вы используете 10 переменных и 20 числовых значений.

А в примере из №27 столько же переменных, но 10 числовых значений.

Можно ли говорить об идентичности и, как следствие, о корректности сравнения?

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

Piskunov пишет:

На десяти переменных уже один процент экономия!

Не хитрите! Сравнивать тма можно именно память переменных, а программы там просто разные, чего их сравнивать-то?

Andrey-S
Offline
Зарегистрирован: 02.01.2015

Мужики, а вот объясните для таких как я такой момент: вот память делится на области <<.data   .bss   heap и stack>>... А как идет распределение этой памяти внутри, допустим, УНО? Вот в УНО 2кб ОЗУ памяти и 32кб флэша... Ну и как идет распределение? Какая область всегда фиксирована или таковой нет? 

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Таковой нет ни у УНО, ни у кого ишо. Все цифирьки относительны и зависят от потребления памяти скетчем.

Andrey-S
Offline
Зарегистрирован: 02.01.2015

Arhat109-2 пишет:

Таковой нет ни у УНО, ни у кого ишо. Все цифирьки относительны и зависят от потребления памяти скетчем.

Тобишь границы раздела памяти на рисунке в первом посте ВСЕГДА плавающие?

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Конечно. Даже начало рисунка у разных МК - разное. Средняя точка (откуда растет стек вниз) - это общий объем памяти у конкретного МК - он тоже разный. Крайняя правая - это "предел" для 16-битного адреса 65536 байт. Но "вовсе не предел" для МК "в общем", ибо у некоторых есть регистры, расширяющие адрес до 24 бит.. а xmem шина позволяет левой-задней ногой, добавить скажем к Мега2560 памяти хоть "сколько хочешь" и практически "как родную". Рисовал себе контроллер статической памяти на 256кб, а в сети видел поделки на 512кб оперативы для Мег.

Andrey-S
Offline
Зарегистрирован: 02.01.2015

Arhat109-2 пишет:

Конечно. Даже начало рисунка у разных МК - разное. Средняя точка (откуда растет стек вниз) - это общий объем памяти у конкретного МК - он тоже разный. Крайняя правая - это "предел" для 16-битного адреса 65536 байт. Но "вовсе не предел" для МК "в общем", ибо у некоторых есть регистры, расширяющие адрес до 24 бит.. а xmem шина позволяет левой-задней ногой, добавить скажем к Мега2560 памяти хоть "сколько хочешь" и практически "как родную". Рисовал себе контроллер статической памяти на 256кб, а в сети видел поделки на 512кб оперативы для Мег.

Спасибо за объяснение! Для меня это пока тёмный лес с добавлением памяти..Надеюсь не понадобится и функционала меги будет хватать). В принципе тогда осталась одна непонятка в моей голове... Вот глобальные и статические переменные занимают .data и .bss... Предположим, что в нашем скетче их настолько дохрена, что вместе они съедают 1кб памяти... остается (для УНО) еще 1кб, который распределяется между "кучей" и "стеком" так? Если так, то можно ли забить "стеком" (а как я понимаю стэк - это локальные переменные внутри loop()) весь остаток памяти или же "куча" всегда будет существовать? И исходя из этого "Далее идёт т.н. куча – это место, откуда Вам будет выделяться память, если Вы запросите её при  помощи оператора new или функции malloc (calloc, realloc и т.п.). " А для чего делать память, которую нужно специальным образом запрашивать?

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Andrey-S пишет:
Спасибо за объяснение! Для меня это пока тёмный лес с добавлением памяти..Надеюсь не понадобится и функционала меги будет хватать). В принципе тогда осталась одна непонятка в моей голове... Вот глобальные и статические переменные занимают .data и .bss... Предположим, что в нашем скетче их настолько дохрена, что вместе они съедают 1кб памяти... остается (для УНО) еще 1кб, который распределяется между "кучей" и "стеком" так?

Так.

Andrey-S пишет:
Если так, то можно ли забить "стеком" (а как я понимаю стэк - это локальные переменные внутри loop()) весь остаток памяти или же "куча" всегда будет существовать?

Легко. Стек это не только локалы из loop(), но и все локалы, а также адреса возвратов всех вызываемых функций/процедур как из loop() так и откуда-то ишо. Самый большой стек разматывается в самом глубоком вызове и содержит все локалы и адреса возврата "вверх" до самого main(), которого вы не видите и из которого и вызываются setup() и loop() на самом деле.

Andrey-S пишет:
И исходя из этого "Далее идёт т.н. куча – это место, откуда Вам будет выделяться память, если Вы запросите её при  помощи оператора new или функции malloc (calloc, realloc и т.п.). " А для чего делать память, которую нужно специальным образом запрашивать?

С/С++ так устроены. :)

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

Andrey-S пишет:

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

Для того, чтобы её можно было освобождать.

Когда Вы сами запрашиваете и освобождаете, это не совсем тоже самое, как если Вам раз на всю жизнь выделили (статическая) или Вам выделяют и освобождают когда системе захочется (стек). 

Самому запрашивать и освобождать - гибкости поболее будет, не находите?

Andrey-S
Offline
Зарегистрирован: 02.01.2015

ЕвгенийП пишет:

Для того, чтобы её можно было освобождать.

Когда Вы сами запрашиваете и освобождаете, это не совсем тоже самое, как если Вам раз на всю жизнь выделили (статическая) или Вам выделяют и освобождают когда системе захочется (стек). 

Самому запрашивать и освобождать - гибкости поболее будет, не находите?

Не понятно... Если провести аналогию нууу скажем с пустой банкой (это все ОЗУ) и налитую в ней воду (переменные которые занимают память), то получим, что первоначально залитая вода будет являться .data и .bss ... Далее, берем стек - это та вода, которую мы будем то доливать, то отливать из банки, но с ограничением, что в банке должно остаться не менее, чем .data+.bss воды...Вопрос, как представить в этом случае"кучу", если, конечно, можно проводить такое грубое сравнение?

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

не совсем так. Если уж сравнивать с банкой, то .data,.bss - это тот "миниум" что у вас туда залит сразу по факту компиляции скетча. А то что стек - это то, что доливается и отливается БЕЗ вашего участия: доливается при каждом вызове и отливается при завершении каждой функции, даже если у неё нет локалов вовсе - доливается точка возврата и отливается она же. А вот то, что вы доливаете "ручками" - это как раз "куча" .. и важно не забывать отливать .. ручками, ибо само оно не выльется, в отличии от "умных" языков типа Java, PHP, JS и т.д.

Другое дело, что в целом, программа оперирует неким объемом данных. Так, к примеру, если вы все-все объекты объявите только глобально (статически), то все они лягут в .data,.bss и компилятор вам сразу радостно сообщит "сколько оно в граммах". А если вы всё-всё будете размещать на стеке локально, то вам придется таскать и много одни и теже значения через параметры вызовов .. стека израсходуется "дофига", кода тоже, но .. компилятор врядли вам что-то радостно сообщит. А если вы всё-всё будете размещать в куче, то вам ещё за всем этим придется следить и освобождать когда надо + код, который будет размещать и освобождать память.

Как итог:

для МК все что можно желательно размещать статически. Особенно это относится к данным, имеющим "долгий срок жизни". Дополнительно экономите на коде, формирующем параметры вызовов и возвращение данных - меньшще параметров - меньше гемморой. Всё, что можно "вычислить", да ещё и "внутри функции" и оно снаружи не требуется - желательно размещать локально (все равно по выходу можно выбросить). А вот кучей имеет смысл пользоваться только И только тогда, когда у вас всё в глобалы "не лезет" и время жизни данных позволяет чередовать их размещение в памяти ручками. Ибо - ДОРОГО (по скорости И объему кода).

Следствие: наследование и виртуализация (полиморфизм) ООП - надо использовать только при крайней необходимости, в противном случае получаете избыточное кодирование и завышенные требования к памяти.

:)

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

Arhat109-2 пишет:

для МК все что можно желательно размещать статически. 

Именно после таких советов и появляются библиотеки отжирающие половину памяти контроллера. А народ недоумевает, и библиотека есть и даже работает вроде, только в примитивном проекте с её участием памяти нет. 

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

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Да, хорошее замечание, спасибо. Но его надо акцентировать несколько иначе, а именно:

Никогда не выделяйте память под буферное использование ВНУТРИ библиотек! Библиотека должна работать с УКАЗАТЕЛЯМИ, и только с ними. А где и сколько выделить памяти - решение исключительно того, кто пользует библиотеку.

При выдерживании такого условия - работа через указатели - никакой "лишней памяти" никакая библиотека "отожрать" не способна. Проблема - в избыточной буферизации. И, в частности для меня, это главный критерий КАЧЕСТВА библиотеки: выделяет ли она буферную память "молча" или работает с указателями и позволяет мне решить вопрос выделения памяти .. и часто вполне достаточно .. статически. :)

Seva1800
Offline
Зарегистрирован: 18.12.2015

Полезная информация. Спасибо.
Насколько я понимаю получается, что глобальные переменные фактически выполняют функцию передечи данных между процедурами, а локальные переменные являются своего рода вспомогательными временными вещами для проведения вычисленря внутри функций, которые (переменные) освобождают помять по выходу из процедуры. Также локальные переменные в свою очередь могут выполнять функцию передачи данных между самими функциями, при вызове одной ф из другой ф. Однако, просто так плодить локальные переменные я так понимаю не стоит. Например, в функции, где используются 3 глобала и причем 2 из них как чисто информационые (как флаги), то нефиг делать функцию с 3 входными параметрами, достаточно одного.

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Да, именно так.

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

1. Если ваш скетч писан "в лоб и быстро" И влезает в заданные ресурсы МК - то и пофиг. Далее читать НЕ ТРЕБУЕТСЯ.

2. Если нет:

2а: ... И он пользует много сторонних либ, то ищем "самый жрущий" кусок и заменяем либу(ы) на те, которые не создают собственных буферов, не пользуют виртуальное или классовое программирование "почем зря". Если этого достаточно - то и пофиг, далее не читаем.

2б: .. Алгоритм сложен и жрет много ресурсов - ищем решение под названием "алгоритмическая оптимизация", а по-просту заменяем и/или оптимизируем алгоритм (часто это одно и тоже). Как правило, достаточно погуглить и/или применить Касьянова.

3. Если это не помогает, применяем кодовую оптимизацию и оптимизацию размещения данных, помня что всякий перенос принятия решения на уровень вниз - это дополнительные затраты кода, скорости, данных и как правило ОДНОВРЕМЕННО и то и другое:

"Уровни принятия решений":

0 - "препроцессор". Этап, проходящий ДО компиляции скетча. Опция компилятора -E позволяет "посмотреть", какой код в реальности будет компилироваться.

Как пример: указание номера пина константой препроцессора означает, что этот номер НЕ предполагается к изменению "по ходу пьесы" и его имя - исключительно "удобства для". В С++ есть префикс const для тех же самых целей.

Для "действий" это же самое превращается в решение вопроса inline (макрос препроцессора) или "вызываемое нечто" (функция, процедура, метод, виртуальный метод) .. Цена вопроса, и решение достаточно просто: каждая инструкция МК практически выполняется за 1-2 такта. Часто 1. 2 такта - доступ к памяти глобала, и 3 такта - косвенный доступ к памяти (виртуально). Вызов и возврат для функции/метода БЕЗ параметров 4-5 тактов вызов + 4 такта возврат. Каждый параметр добавляет ещё по 3-4 такта и 6 байт на организацию его передачи (итого "от" 10-12 тактов!). Отсюда: действие над 1 параметром, которое в реализации короче 5-6-8 команд МК - надо разворачивать inline или оформлять в макрос. По времени - явный выигрыш, а по коду "то на то и получится" по сравнению с ценой вызова. Надо отметить, что компилятор зачастую эту задачу решает успешно и сам.

1 - "компилятор - статика". Этап, позволяющий компилятору принять все решения о месте, способе оптимизации и пр. на этапе компиляции кода. Сюда относятся все глобалы, простые и прямо отнаследованные классы, простые функции и простые (не виртуальные и не перекрытые!) методы классов. В таком виде "классовое" программирование - исключительно "удобства для", и на качество кода влиять не должно. Компилятор - умный, сам все распределит.

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

2 - "компилятор - автоматика". Это то, что будет автоматически создано или удалено - да, те самые "локалы" или стек. Если нам не требуется "длительное" хранение значений, и, тем более, если они ВЫЧИСЛЯЮТСЯ внутри функции и не требуются далее вне её .. то самое то - предоставить компилятору это как локал с автоматическим выделением/освобождением места. Оно ещё и экономиться будет. Регистровый пул - компилятором рассматривается как часть стека(!) и соответственно, с высокой долей вероятности, 3-4-6 байт локалов будет размещено на регистрах, и вовсе без расходов памяти стека.

К сожалению, (может только у меня avr-gcc v4.7.1) но компилятор НЕ размещает в регистрах данные объявленные как struct{} или union{} .. такое сразу лезет в память, даже если оно из пары байт. аналогично про длинные целые: чем их меньше - тем лучше. Ну нет у МК команд для работы с ними, а компилятор не понимает, что long это просто 4 байта... расход регистров и всего прочего - феноменален. Писал уже разрабам .. ответили, что "можно но сложно" поправить. может быть .. когда-нибудь ..

3 - "динамика или runtime". Это то, о чем компилятор не в состоянии принять хоть какое-то решение и выделение памяти, её освобождение, какой метод надо вызвать именно тут .. все решается "на скаку", в смысле по ходу работы программы. Это как раз все те "прелести" С++, которыми так любят "жонглировать" начинающие программисты, слегка освоившие С++, но не имеющие реального опыта писания под МК: это тот самый "полиморфизм", "множественное наследование", "виртуальные функции" и пр. Средства, которые позволяют в runtime вызвать метод дочернего класса, по объекту .. решается в коде как правило исключительно введением дополнительной косвенности, а то и двух уровней.

Применятся в МК должно только в исключительных случаях. Как пример, номер пина, который не просто изменяется "по ходу пьесы" - вот тут надо дернуть за 3-й, а теперь точно также дергаем за 5-й, а когда способ дерганья также обязан изменяться "по ходу пьесы": если получили такой сигнал, то дергаем 3-й пин левой ногой, а иначе 3-й пин правой ногой или 5-й пин лупим кувалдой. Когда скетч обязан решать задачу выбора методики И номера - динамически, по мере работы проги и получения ею данных. Как правило такое встречается крайне редко.

Желание же применить виртуализацию и прочие плбшки С++ из runtime реализаций "удобства для" - это и есть "жонглирование" языком БЕЗ какого либо его понимания.

 

Andrey-S
Offline
Зарегистрирован: 02.01.2015

Arhat109-2 пишет:

 А вот то, что вы доливаете "ручками" - это как раз "куча" .. и важно не забывать отливать .. ручками, ибо само оно не выльется, в отличии от "умных" языков типа Java, PHP, JS и т.д.

А что значит доливаете "ручками"? Вот работает какой-то код, состоящий из глобалов и локалов...уровень воды в банке постоянно бегает как уровень тосола в бачке а/м... Как туда что-то доливается ручками, если все уже залито и пашет? Я что-то не туда думаю похоже...

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

Andrey-S,

ну. вот смотрите, Вам в трёх разных участках программы нужно на короткий срок (для какой-то обработки) массив размером в 1К.

Разместить три массива заранее статически Вы не можете - места столько нет. На стеке - можно, если вся обработка в одной функции, а если в нескольких, там уже не всё так очевидно.

В этой ситуации Вы можете по мере необходимости запросить 1К и освободить как только он больше не нужен. И поступать так хоть десять раз. Таким образом Вы сможете во всёх трёх местах всё посчитать, а память будет использована одна и та же.

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Да. И это называется "ручками". Ручками запросили, ручками освободили .. :)

Andrey-S
Offline
Зарегистрирован: 02.01.2015

Arhat109-2, ЕвгенийП, спасибо за объяснения... В принципе в теории стало все намного понятнее, вот еще бы простейший примерчик, чтобы воочию увидеть этот "изюм"... Если с .data и .bss все понятно с самого первого поста, то эта "борьба" стэка с кучей по толканию "промежутка с восклицательным знаком" никак не укладывается у меня в голове... Стэк сам по себе уже с плавающей границей по ходу выполнения программы и почему бы в него точно также не заносить массив в 1К, если по завершении функции этот массив все равно улетит,т.к. он локальный? 

ЕвгенийП пишет:

В этой ситуации Вы можете по мере необходимости запросить 1К и освободить как только он больше не нужен.

Если мы создадим локальный буфер на 1кб внутри стэка, то при условии, что стэк располагает таким объемом, место выделится-буфер создастся-отработает в функции и затем исчезнет, освободив наши 1кб Так ли это? А если стэк не располагает таким объемом? МК зависнет, начнет глючить и т.п. скорее всего... Переходим к куче: запросили 1кб памяти и если он есть, то снова буфер отрабатывает исчезает и все прекрасно...А если не располагает? Я понимаю, что рассуждаю скорее всего не в той плоскости ввиду полной некомпитентности в данном вопросе, но может статейку посоветуете, в которой "колхозникам" объясняют на их языке, чтобы я тут не флудил.

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Все верно на самом деле, правильно рассуждаете. Да, если на стеке места не хватит - скорее всего это приведет к внезапной перезагрузке МК или к его ну очень странному поведению. А если не хватит места в куче, то вам его просто не выдаст функция выделения места в куче .. malloc() к примеру (есть и другие аллокаторы). Но, если место в стеке будет освобождено автоматически по завершению вызванной функции, то место в куче вам надо освобождать явным вызовом деаллокатора free() (? не помню), иначе оно так и останется занятым. Да, и вовсе не факт, что получив однажды место в куче под свой килобайтный буфер, при повторном запросе (освободив предварительно!) вы его снова получите! Есть такое понятие как "фрагментация куч".. место под буфер может внезапно оказаться разделенным на 2 куска по 500 байт .. и "фсё", вигвам а не буфер. :)

Я поэтому старательно обхожу вопросы работы с кучей, что имею опыт писания кучевых диспетчеров типа malloc() и повидал их несколько .. так, к примеру, косяк в виндовом malloc() преспокойно кочевал из версии в версию с w3.11 .. вплоть до XP :) Поверьте, там далеко не все так "просто и очевидно".. и дешево. Полноценный диспетчер должен вести списки выделенных кусков (указатель на начало куска, размер куска, указатель следующего/предыдущего куска и т.д. - только "описание" куска - очереди, коллекции - "наше всё"), вести список "дырок", уметь сливать соседние дырки в одну большую, иметь стратегию выделения/поиска подходящего куска и они есть разные .. и мн.др. Всё это требует достаточных ресурсов МК, которых не так чтобы многа.

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