Странное поведение счетчика int в цикле for

smailik
Offline
Зарегистрирован: 08.04.2021

Скетч вот такой
 

void setup()
{
  Serial.begin(115200); // Initialize the serial line
  
  Serial.println("START");
  for (int i = 32000; i >= 0 && i <= 32767; i++)
  {
    Serial.println(i);
  }
  Serial.println("DONE");

}

void loop()
{
}

Проще заставить его нормально работать вот так
 

void setup()
{
  Serial.begin(115200); // Initialize the serial line
  
  Serial.println("START");
  for (long i = 32000; i >= 0 && i <= 32767; i++)
  {
    Serial.println(int(i));
  }
  Serial.println("DONE");

}

void loop()
{
}

И больше с ним не заморачиваться.
Но пост DetSimen натолкнул меня на мысль, что если включить варнинги в компиляторе, то, возможно он будет выдавать какие-либо предупреждения о том, что код "странный" и может после оптимизации работать не так, как ожидалось.
Интерес уже академический. 

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

smailik
Offline
Зарегистрирован: 08.04.2021

Почему то не получается картинку вставить.
У меня так же, только галка напротив загрузки не стоит.

 

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

у меня, правда, это выглядит несколько по-другому

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

smailik пишет:
У меня так же

И шо, компилятор молчит, как Лига Наций? 

smailik
Offline
Зарегистрирован: 08.04.2021

Попробовал вместо int для счетчика задать byte. И компилятор вывалил предупреждение про усечение типа.
Так что варнинги включились. Просто компилятор в коде не видит ничего странного. А на этапе оптимизации оптимизирует его, выкидывая условия проверки :)

Еще раз всем спасибо :)

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

smailik,

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

Вот Вы написали

smailik пишет:

1. "Неправильное" написание мною кода.
2. Как следствие пункта 1 - кривая работа оптимизатора, выкинувшего все условия проверки значения счетчика

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

Дело, конечно, Ваше, но такой подход как бы "расставляет точки над всеми буквами", закрывает проблему и (это самое важное!!!) лишает Вас даже возможности таки разобраться и понять свои ошибки.

Правильно было написать вот как:

1. Неправильное написание кода
2. Как следствие, оптимизатора выкинул проверки.

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

Ведь что он сделал? Он выбросил обе проверки в выражении

for (int i = 32000; i >= 0 && i <= 32767; i++)

И что? Какую из проверок он не должен был выбрасывать? В чём его косяк-то? Вы правда не понимаете, что он абсолютно правильно выбросил обе проверки?

Конечно, ошибки в компиляторе возможны, но поверьте моему 42-летнему опыту программирования - в 99,(9)% случаев за ошибку компилятора люди принимают то, что он поступил в соответствие с "правилами языка программирования", а не с "вашими представлениями об этом языке", которые (представления), на поверку, оказались не совсем верными.

smailik
Offline
Зарегистрирован: 08.04.2021

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

мне понравилась Ваша манера обсуждения

Спасибо, очень приятно слышать подобное от опытного программиста и (как я уже успел понять) одного из гуру этого форума.

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

Мне хочется Вам помочь в понимании некоторых вещей в программировании, которые я бы отнёс к "философии профессии". Однако, боюсь, что выглядеть это будет как некое менторство. ... В общем, этот пост исключительно доброжелательный и прошу не воспринимать его никак иначе.

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

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

Здесь проявилась свойственная людям тенденция переваливать свою вину на другого.

Мне это не свойственно. Я признаю, что ошибка была моя. Ошибка была в непонимании некоторых аспектов работы оптимизатора.
Условие i <= 32767 однозначно лишено смысла применительно к int.
Условие i >= 0 полностью смысла не лишено, вдруг я внутри цикла как-то счетчик меняю? Но это опять же бэд стайл.

Так что, я считаю, что в своей ошибке полностью разобрался.

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

Честно  признаюсь, что с Си я знаком очень поверхностно. И аспекты работы его оптимизатора были точно неизвестны. Можно, конечно, было предположить, что компилятор языка, известного своей эффективностью и производительностью, будет код жать до упора. Ну вот :)

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

Спасибо за помощь. Был рад пообщаться.

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

smailik пишет:

Условие i >= 0 полностью смысла не лишено, вдруг я внутри цикла как-то счетчик меняю? Но это опять же бэд стайл.

Лишено. Вы никак не меняете, кроме ++,  и оптимизатор это отлично видит. Поэтому, с точки зрения языка, i никогда не может стать отрицательным. Так что это (отбрасывание) вполне эквивалентное преобразование программы (повторюсь, с точки зрения языка).

v258
v258 аватар
Offline
Зарегистрирован: 25.05.2020

smailik пишет:

Условие i >= 0 полностью смысла не лишено, вдруг я внутри цикла как-то счетчик меняю? Но это опять же бэд стайл.

Насколько я понимаю, внутри цикла счетчик изменять нельзя

smailik
Offline
Зарегистрирован: 08.04.2021

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

smailik пишет:

Условие i >= 0 полностью смысла не лишено, вдруг я внутри цикла как-то счетчик меняю? Но это опять же бэд стайл.

Лишено. Вы никак не меняете, кроме ++,  и оптимизатор это отлично видит. Поэтому, с точки зрения языка, i никогда не может стать отрицательным. Так что это (отбрасывание) вполне эквивалентное преобразование программы (повторюсь, с точки зрения языка).

Провел эксперимент.

Делаем вид, что как-то изменяем счетчик внутри цикла. Чтобы усложнить работу компилятору, менять будем не через константу, а с помощью другой переменной.

 

void setup()
{
  int d;
  
  d=0;

  Serial.begin(115200); // Initialize the serial line
  
  Serial.println("START");
  
  for (i=32000; i >= 0 && i<=32767 ; i++)
  {
    i=i+d;
    Serial.println(i);
  }
  Serial.println("DONE");

}

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

А теперь просто объявим i , как глобальную переменную.

int i;
void setup()
{
  Serial.begin(115200); // Initialize the serial line
  
  Serial.println("START");
  for (i = 32000; i >= 0 && i <= 32767; i++)
  {
    Serial.println(i);
  }
  Serial.println("DONE");

}

void loop()
{
}

Все хорошо, скетч отрабатывает "правильно", вывод на 32767 заканчивается.
Ладно, прекращаю  рукоблудие :)
 

smailik
Offline
Зарегистрирован: 08.04.2021

Del

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

smailik пишет:

Провел эксперимент.

Делаем вид, что как-то изменяем счетчик внутри цикла. Чтобы усложнить работу компилятору, менять будем не через константу, а с помощью другой переменной.

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

Замените строку №5 на

d=random(1, 5);

и он Вас удивит.

smailik пишет:
прекращаю  рукоблудие

Напрасно. В проблеме Вы так и не разобрались. Значит, она ждёт своего часа, чтобы проявиться снова.

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

v258 пишет:

Насколько я понимаю, внутри цикла счетчик изменять нельзя

Ну, я не помню, не запретил ли это Клапауций. Но, если не запретил, то можно.

smailik
Offline
Зарегистрирован: 08.04.2021

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

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

Замените строку №5 на

d=random(1, 5);

и он Вас удивит.

smailik пишет:
прекращаю  рукоблудие

Напрасно. В проблеме Вы так и не разобрались. Значит, она ждёт своего часа, чтобы проявиться снова.

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

 

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

smailik пишет:
Осталось понять

Во!!!

А Вы говорите:

smailik пишет:
прекращаю  рукоблудие

Рано ещё! Не всё ещё понято!

smailik пишет:
почему простое изменение области видимости переменной приводит ...

Ну, вот опять 25! А кто Вам сказал, что к результату приводит "изменение области видимости"? А может другое чего?

Вы опять поставили некорректный эксперимент и на основании него сделали неправильный вывод. Посмотрите на код: 

void setup() {
	Serial.begin(115200);
	Serial.println("START");
	for (static int i=32760; i >= 0 && i<=32767 ; i++) {
		Serial.println(i);
	}
	Serial.println("DONE");
}
void loop(){}

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

По ходу, не в области видимости дело!

smailik
Offline
Зарегистрирован: 08.04.2021

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

Вы опять поставили некорректный эксперимент и на основании него сделали неправильный вывод. Посмотрите на код: 

void setup() {
	Serial.begin(115200);
	Serial.println("START");
	for (static int i=32760; i >= 0 && i<=32767 ; i++) {
		Serial.println(i);
	}
	Serial.println("DONE");
}
void loop(){}

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

По ходу, не в области видимости дело!

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

Определение счетчика через static не изменяет область видимости переменной (она также видна только в теле цикла), но изменяет время ее жизни (до завершения цикла -> до завершения программы) и место хранения (стек -> область данных). Таким образом, статический счетчик становится неким аналогом глобальной переменной, но со своей ограниченной областью видимости.

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

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

void setup() {
  int *p;
  Serial.begin(115200);
  Serial.println("START");
  for (static int i=32760; i >= 0 && i<=32767 ; i++) {
    Serial.println(i);
    p=&i;
  }
  Serial.print("i after cycle=");
  Serial.println(*p);
  *p=*p-1;
  Serial.print("i after decr=");
  Serial.println(*p);
  Serial.println("DONE");
}
void loop(){}

Вот такая версия.
 

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Женя! Вот пример новичка, которого мы тут много лет ждали! (ТС - не возгордись! ;)) ) Нормальный, не обидчивый, без понтов, с хорошей начальной подготовкой и желающий учиться. Да еще и понятливый.

ОДИН за 5 лет. :((

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

smailik пишет:
изменяет время ее жизни (до завершения цикла -> до завершения программы)

Точно!

smailik пишет:
и место хранения (стек -> область данных)

А вот это - нет. Язык никак не определяет место хранения. В стандарте языка слово "стек" используется только в контексте порядка вызовов конструкторов и деструкторов и в контексте обработчиков исключений. Никаких требований хранить что-то на стеке там нет - как компилятор сделает, так и и будет. Имеет право хранить где хочет.

smailik пишет:
оптимизатор компилятора не рискует

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

Тут остался ещё один момент. Вот мы говорим, что условие i>=0 всегда истинно. Потому эту проверку и выбросили. Тем не менее, i при постоянных ++ рано или поздно таки становится отрицательным. Вам понятно почему здесь нет противоречия? Или пояснить?

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

wdrakula пишет:

Женя! Вот пример новичка

Так ото ж!

smailik
Offline
Зарегистрирован: 08.04.2021

wdrakula пишет:

Женя! Вот пример новичка, которого мы тут много лет ждали! (ТС - не возгордись! ;)) ) Нормальный, не обидчивый, без понтов, с хорошей начальной подготовкой и желающий учиться. Да еще и понятливый.

ОДИН за 5 лет. :((

Ух ты! Очень приятно :) Спасибо за столь лестную оценку.
Гордыня - грех :) Как и обида - чувства энергозатратные  и непродуктивные.

smailik
Offline
Зарегистрирован: 08.04.2021

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

smailik пишет:
и место хранения (стек -> область данных)

А вот это - нет. Язык никак не определяет место хранения. В стандарте языка слово "стек" используется только в контексте порядка вызовов конструкторов и деструкторов и в контексте обработчиков исключений. Никаких требований хранить что-то на стеке там нет - как компилятор сделает, так и и будет. Имеет право хранить где хочет.

 

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

1. Собственно сам исполняемый код
2. Область данных - статические и глобальные переменные
3. Куча - динамическое выделение памяти
4. Стек вызовов - параметры функций и локальные переменные
Может, чего и напутал. 

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

smailik пишет:
оптимизатор компилятора не рискует

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


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

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

Тут остался ещё один момент. Вот мы говорим, что условие i>=0 всегда истинно. Потому эту проверку и выбросили. Тем не менее, i при постоянных ++ рано или поздно таки становится отрицательным. Вам понятно почему здесь нет противоречия? Или пояснить?

int x;

x = 32767;
x = x + 1;       // x теперь равно -32,768

x = -32768;
x = x - 1;       // x теперь равно 32,767 
 

Оно? Или Вы имели ввиду что-то другое?

 

SergeiL
SergeiL аватар
Offline
Зарегистрирован: 05.11.2018

wdrakula пишет:

Ой! В 1984-ом году я учился в матшколе и было мне 15 лет. Вместо быдляцкого УПК, у нас было программирование, которое вели ребята из ИПМ. Так вот я сломал БЭСМ-6 у них в ВЦ программой НА ПАСКАЛЕ ;))))))). Причем я её на синем бланке отдавал в перфорацию, то есть её много раз проверяли ;)). Но то, что я во вложенных циклах файлы открываю, никто не заметил... Защиты от большого числа открытых файлов в системе не было :(.

В 84-ом любимой игрушкой был Б3-34. В школе фортран. А в 87-ом поступил на вечерний ЛЭТИ и устроился на работу в качестве электромеханика. Когда устраивался рассказал про фортран, сказали что  не актуально :-( у них все на Си и Unix было. А я про Си тогда знал лишь то, что есть система мер и весов Си.  :-)

В обслуживании были: СМ-4 c RSX-11M, СМ-1420 c ДЕМОС то есть UNIX и СМ-1800 c CP/M.

В общем запал тогда на СМ-1420 c ДЕМОС, Ingres DB. Начал учить Си, учиться программировать в Unix  на Си, батники на  сишелл, шелл и т.д. 

Блин более 30 лет прошло - все это актуально и сейчас. :-) 

 

 

 

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

smailik пишет:

int x;

x = 32767;
x = x + 1;       // x теперь равно -32,768

x = -32768;
x = x - 1;       // x теперь равно 32,767 
 

Оно? Или Вы имели ввиду что-то другое?

Оно. Помните мой пост #25 и немного туманные фразы про "странные вещи. Причём у разных компиляторов/оптимизаторов - разные" в районе переполнения? И про болото? Так вот здесь оно самое. То самое болото и странности.

То, что Вы написали (32767+1 = -32768 для 16-битного int) верно для большинства (не для всех) современных компиляторов, но это неверно для языка С++!

Вот смотрите. Чему на самом деле равен результат выражения 32767+1 ? Понятно, что 32768. Но эта величина выходит за рамки типа int16_t - она не представима в нём. Правильно?

Теперь открываем стандарт языка и читаем: "If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined".

Т.е. язык полагает, что поведение в таком случае неопределенно. Хорошо, тогда читаем как именно должны реализовываться подобные случаи. Там же? в стандарте: "This document imposes no requirements on the behavior of programs that contain undefined behavior".

Ну, подводим итоги: результат операции 32767+1 не влазит в тип. Язык считает, что поведение в этом случае неопределенно и не накладывает никаких требований на реализацию таких вещей. Попросту говоря, как разработчики компилятора реализуют, так и правильно, язык тут не при делах.

Отсюда, кстати, и пример b707 - когда в другом компиляторе у него работало по-другому. И ни один из компиляторов не кривой - оба правы. Отсюда же и мои слова про "странности и болото" и совет просто избегать подобных ситуаций. Т.к. разумного, стандартизованного поведения здесь ожидать не приходится.

Вот как-то так.

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

smailik пишет:

1. "Неправильное" написание мною кода.

...

Насчет пункта 1 - попробую слабенько защититься. Код, конечно, "скользкий". Но вот у b707 под Линуксом этот же код скомпилился нормально и отработал ожидаемо. Ладно, согласен, отмаза так себе.

Это вообще не "отмаза", а "антиотмаза".

Правильный код работает предсказуемо и всегда одинаково. Неправильный - зависит от чего угодно: от самого компилятора, от его версии, от ключей компиляции, от погоды на Марсе и т.д...

Так что Ваш аргумент лишь подтверждает, что код неправильный.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

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

Green
Offline
Зарегистрирован: 01.10.2015

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

То, что Вы написали (32767+1 = -32768 для 16-битного int) верно для большинства (не для всех) современных компиляторов, но это неверно для языка С++!


Си ведёт себя так же - разницы нет.
ЕвгенийП пишет:

Отсюда, кстати, и пример b707 - когда в другом компиляторе у него работало по-другому. И ни один из компиляторов не кривой - оба правы. Отсюда же и мои слова про "странности и болото" и совет просто избегать подобных ситуаций. Т.к. разумного, стандартизованного поведения здесь ожидать не приходится.


Пример b707 не корректный. Какой размер int в Линуксе?

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

Тут мудрит оптимизатор. Потому что при opt=1 всё красиво, а уже выше - то что имеем.
Красиво и при opt=s, но только если приказать оптимизатору не трогать i.

 

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

Green пишет:

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

Почему не объясняют? Вы не поняли главной мысли: что должно быть при переполнении в языке не определено! А значит, оставлено на усмотрение реализации. Как реализовали, так и реализовали - имеют право.

Green
Offline
Зарегистрирован: 01.10.2015

Ну так порнография же ж. Два варианта в одном и том же случае.(
Кстати с другим компилятором хотя бы предупреждение выдаёт для случая:

for (int i = 32760; i >= 0 && i <= 32767; i++)
​Warning [766] D:\arc\home\pic\hron628\main.c; 165. degenerate signed comparison

 

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

Green пишет:

Ну так порнография же ж. 

Ну, дык ... "undefined behavior" он такой "undefined" :-)

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

SLKH
Offline
Зарегистрирован: 17.08.2015

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

 

Теперь открываем стандарт языка и читаем: "If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined".

Т.е. язык полагает, что поведение в таком случае неопределенно. Хорошо, тогда читаем как именно должны реализовываться подобные случаи. Там же? в стандарте: "This document imposes no requirements on the behavior of programs that contain undefined behavior".

вот возникает какая-то очень смутная ассоциация со словосочетанием "женская логика".