Форум программистов, компьютерный форум, киберфорум
Наши страницы

C для начинающих

Войти
Регистрация
Восстановить пароль
 
 
Рейтинг: Рейтинг темы: голосов - 35, средняя оценка - 4.77
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
#1

[Задача] Адресная арифметика - C (СИ)

08.03.2012, 12:38. Просмотров 4788. Ответов 49

Просьба к модераторам: НЕ надо перетаскивать в разделы типа "Си\Си++ для экспертов"

Пример возник на основе реальной программы. Пример содержит ошибку, а потому не факт, что повторится на всех компиляторах. В моём случае ошибка проявлялась на i386-linux32 при использовании компилятора gcc.

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
 
char a = 5;
char b = 10;
 
int main (void)
{
  char tmp;
 
  /* Для контроля убедимся, что "a" и "b" оказались в соседних
   * ячейках памяти. Разница указателей должна равняться единице.
   * Именно единицу мы используем в следующем операторе */
  printf ("&b - &a = %d\n", &b - &a);
 
  /* Записываем в переменную "b" */
  *(&a + 1) = 11;
 
  /* Читаем из переменной "b" */
  tmp = b;
 
  printf ("tmp = %d\n", tmp);
  printf ("b = %d\n", b);
  return 0;
}
Запуск без оптимизаций:

Код
$ gcc t.c
$ ./a.out 
&b - &a = 1
tmp = 11
b = 11
Запуск с оптимизациями:

Код
$ gcc t.c -O3
$ ./a.out 
&b - &a = 1
tmp = 10
b = 11
Вопрос. В программе перед printf'ами есть операция "tmp = b;". Почему напечатались разные значения для "tmp" и "b"? Эти значения далеко не рандомные: одно напечатанное значение соответствует "старому" значению переменной "b", а другое - "новому"

Добавлено через 29 минут
Традиционная просьба прятать догадки и ответы под CUT'ом
0
Лучшие ответы (1)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
08.03.2012, 12:38
Я подобрал для вас темы с готовыми решениями и ответами на вопрос [Задача] Адресная арифметика (C (СИ)):

адресная арифметика - C (СИ)
int funk(char *s) { char *p=s; while(*p) p++; return p-s; } Если строка состоит из пяти...

Адресная арифметика - C (СИ)
Можно ли в C++ в массиве произвольного типа использовать адресную арифметику?Например, так: TYPE*t; int c; t+=c*sizeof(TYPE);

Адресная арифметика - C (СИ)
Объясните пожалуйста вот эту строчку кода return allocp -n: Почему просто не возвратить указатель на блок выделенной памяти,а...

Указатели и адресная арифметика - C (СИ)
Помогите с заданием Язык C. Задание звучит так: 2) Для этого фрагмента программы написать значения каждой переменной после каждого...

Адресная арифметика и массивы - C (СИ)
Нужно с помощью указателя организовать ввод и вывод матрицы. Индексную адресацию не использовать. Вот мой код, что не так в нем? ...

Указатели, адресная арифметика - C (СИ)
Ребята, помогите плиз вот с таким вот заданием... Написать программу, которая поочередно выводит в шестнадцатеричной форме значения...

49
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
08.03.2012, 16:56  [ТС] #16
Лучший ответ Сообщение было отмечено автором темы, экспертом или модератором как ответ
Цитата Сообщение от NoMasters Посмотреть сообщение
В любом случае нельзя делать никаких предположений насчёт взаимного расположения элементов из не непрерывных областей памяти, так что не очень понятно о чём спор...
Когда вводится операция "delta = &a - &b;", то ты не делаешь никаких предположений. Ты это вычисляешь как факт (а не предположение). Далее, запустив конкретный пример на конкретном компиляторе и посмотрев на код ты опять работаешь с фактом, а не с предположением. Ну а сказать про то что нельзя делать какие-то предположения - это просто попытка отмахнуться, не пытаясь вникнуть в суть. Кстати. спора тут нет

Как оно на самом деле
По стандарту языка Си адресная арифметика "адрес + целое" определена только в тех случаях, когда и "адрес" и результат операции являются адресами внутри одного и того же объекта (переменной языка). В нашем случае значение "&a + 1" вываливается за границу переменной "a", а потому результат такой операции неопределён.

Данным свойством стандарта руководствуется компилятор при оптимизациях на современных процессорах, которые имеют возможность параллельного исполнения инструкций. Современные Intel'овские процессоры являются суперскалярами, которые состоят из нескольких исполняющих устройств, позволяющих исполнять операции в параллель. Для подобных процессоров компилятор пытается переупорядочить порядок исполнения операций, чтобы более долгие по времени исполнения операции исполнились как можно раньше (а в пвраллель будут исполняться более быстрые операции). Разумеется, такие перестановки допутимы только в тех случаях, когда компилятор может доказать, что перестановка операций не приведёт к тому, что изменится результат программы.

Операция load (чтение из памяти) в современных процессорах является самой медленной операцией, потому как в случае отсутсвия данных в кэше операция исполняется чуть ли не 100 тактов. Это обусловлено сильно различающейся тактовой частоты процессора и памяти (а так же накладные расходы на работу контроллера памяти). Поэтому компилятор старается load'ы вытащить вперёд по исполнению. При этом требуется проверка на то, а не конфликтуют ли load'ы со store'ами (операциями записи в память). Т.е. если емеется операция записи в память, которая пишет в ту же ячейку памяти или возможно пишет в ту же ячейку памяти, то переставлять её с операцией load нельзя. В нашем случае компилятор видит операцию записи в память по адресу "&a + 1" и операцию чтения из памяти по адресу "&b". Пользуясь указанным выше свойтсвом стандарта компилятор, немного его перефразируя, трактует как "операции чтения и записи в разные объекты никогда не конфликтуют между собой и их всегда можно переставлять". Другими словами, если операции чтения и записи зацеплены за разные объекты, то при любой адресной арифметике можно считать, что их можно переставлять. Но это не так, если мы имеем дело, к примеру, с массивом. В этом случае чтение элемента a[i] можно переставлять с записью элемента a[j] только в том случае, если компилятор сможет доказать, что во всех случаях i != j. Поэтому в нашем примере компилятор переставил местами операторы "*(&a + 1) = 11;" и "tmp = b;", поскольку с точки зрения стандарта он имеет на это право. Ну и если кто-то посмотрит построенный код, то в явном виде это дело увидит. А вот когда есть просто указатель, взявшийся из какой-нибудь внешней функции, то компилятор НЕ имеет права переставлять store по этому указателю с любым load'ом, потому как он не сможет доказать, что эти обращения в память не конфликтуют между собой

История данного примера была следующая. Давным-давно имелась программа, написанная на ассемблере. В программе были два массива "a" и "b" одинакового размера. Была некая функция, которая выполняла симметричную обработку этих массивов. Поскольку на ассемблере программист сам располагает объекты в памяти, то он сам гарантирует и разницу адресов между объектами. Поэтому для обработки массивов был написан код, в который на регистре передаётся адрес массива "a" через который можно достучаться до объекта "b". Таким образом, не нужно было дополнительного регистра для хранения адреса "b" (т.е. сэкономили на регистрах). Смысл кода был примерно такой (для простоты я его напишу на Си, хотя он был написан на ассемблере):

C
1
2
3
4
5
6
7
8
9
10
11
12
13
int a[10];
int b[10];
 
void func (int *p) /* сюда передаётся адрес массива "a" */
{
  /* В качестве примера просто обнулим оба массива, но реальная
   * логика была более сложная */
  for (i = 0; i < 10; i++)
  {
    p[i] = 0;
    p[i+(&b - &a)] = 0;
  }
}
Далее этот код решили аз ассемблера переписать на Си. Ну и переписали с сохранением этого свойства. Это дело долго работало на 486-м процессоре, который НЕ был суперскаляром, а потому компилятор не занимался перестановкой load'ов и store'ов. А потом при перекомпиляции этого исходника под современные процессоры и возникла ошибка, которую очень долго не могли отловить. В режиме с возможностью символьной отладки из отладчика (т.е. по сути без оптимизаций) ошибка не проявлялась. Внесение дополнительных отладочных печатей приводило к тому, что ошибка не проявлялась. А ковыряться в коде оказалось длительным и сложным процессом
8
NoMasters
Псевдослучайный
1908 / 1119 / 77
Регистрация: 13.09.2011
Сообщений: 3,175
08.03.2012, 17:03 #17
Цитата Сообщение от Evg Посмотреть сообщение
ты не делаешь никаких предположений
Делаю. Именно потому, что компилятор имеет полное право переставить не непрерывные куски памяти любым удобным для себя способом даже во время исполнения. Собственно, в некотором смысле, это и происходит.
0
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
08.03.2012, 17:07  [ТС] #18
Цитата Сообщение от NoMasters Посмотреть сообщение
Именно потому, что компилятор имеет полное право переставить не непрерывные куски памяти любым удобным для себя способом даже во время исполнения
Ну переставил он удобным для себя способов. В итоге переменная delta вместо 1 стала равна -1, 10, 100 или сколько угодно. Операцией (&a + delta) делается обратное действие и получается точный адрес переменной b. Ну сделали мы вместо переменной delta константу -1, 10, 100 или что там получилось. Это никак не объясняет причину того, что после операции "tmp = b" получились разные значения в переменных "tmp" и "b".
0
NoMasters
Псевдослучайный
1908 / 1119 / 77
Регистрация: 13.09.2011
Сообщений: 3,175
08.03.2012, 17:12 #19
Evg, delta может меняться в процессе исполнения.
0
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
08.03.2012, 17:22  [ТС] #20
Цитата Сообщение от NoMasters Посмотреть сообщение
Evg, delta может меняться в процессе исполнения.
Ну-ну

Из квантовой теории следует, что если я с разбегу долбанусь в стену, то существует ненулевая вероятность того, что я пройду сквозь стену из-за туннельного эффекта. Если я напишу код "int x = 1000;" то есть шанс, что он отработает неправильно из-за того, что на каком-то компиляторе размер int'а равен 1, или на процессоре байт состоит из 2 битов, а не из 8, или ещё что-то. Могу привести ещё кучу примеров, которые в данном случае будут неуместны (как и твоё возражение)
0
NoMasters
Псевдослучайный
1908 / 1119 / 77
Регистрация: 13.09.2011
Сообщений: 3,175
08.03.2012, 17:28 #21
Evg, ладно, не буду настаивать, пожалуй таким извращение всё равно может заняться разве что llvm с её jit для С.
0
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
08.03.2012, 17:37  [ТС] #22
Цитата Сообщение от NoMasters Посмотреть сообщение
Evg, ладно, не буду настаивать, пожалуй таким извращение всё равно может заняться разве что llvm с её jit для С.
Дело тут не в извращениях, а в ошибке. Причину ошибки я описал в посте #16. Причину возникновения ошибки косвенно ты тоже указал правильно, но из разряда "я где-то слышал, но сам в этом не разбираюсь". Сомневаюсь, что разработчики llvm допустят такую же ошибку. Как показывает практика, авторы проектов, которые работают под многими разными системами, гораздо лучше разбираются во всяких тонкостях чем те, которые всю жизнь работают под одной осью на одном компиляторе
0
alex_x_x
08.03.2012, 17:38
  #23

Не по теме:

Цитата Сообщение от NoMasters Посмотреть сообщение
Evg, delta может меняться в процессе исполнения.
это значит, что адрес одной из переменных поменялся, а если так, то нужно выкинуть компилятор и больше никогда не писать на си

0
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
08.03.2012, 17:48  [ТС] #24
Цитата Сообщение от alex_x_x Посмотреть сообщение
это значит, что адрес одной из переменных поменялся, а если так, то нужно выкинуть компилятор и больше никогда не писать на си
Он скорее всего имел другое. Если взять локальные переменные, то компилятор действительно вправе в одной точке функции держать значение переменной на одном регистре (или участке стека), а в другой точке - на другом регистре (или в другом участке стека), если это не противоречит коду программы. Это же самое может быть и с глобальной переменной. Типа

C
1
2
3
4
5
6
7
8
9
10
11
12
13
int A;
 
void func (void)
{
  ...
  /* В момент работы цикла компилятор в режиме с оптимизациями скорее всего будет
   * держать значение переменной A на регистре, поскольку внутри цикла нет никаких
   * обращений в память по указателям, нет операций вызова, нет каких -либо операций
   * передачи управления под условием и т.п. */
  for (i = 0; i < 10; i++)
    A = A + i;
  ...
}
Скорее всего он что-то прочитал о подобных вещах в книге, но хорошо в суть дела не вник (как и не вник в постановку текущей задачи)
0
alex_x_x
бжни
2454 / 1660 / 84
Регистрация: 14.05.2009
Сообщений: 7,162
08.03.2012, 17:49 #25
Evg, конечно при этом у переменной в регистре отсутствует понятие адреса
начинаю задумываться какие сложности испытывает компилятор при оптимизации кода, однако
0
NoMasters
Псевдослучайный
1908 / 1119 / 77
Регистрация: 13.09.2011
Сообщений: 3,175
08.03.2012, 17:52 #26
Цитата Сообщение от Evg Посмотреть сообщение
но хорошо в суть дела не вник
Скорее не смог толком описать, что хочу сказать
0
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
08.03.2012, 17:58  [ТС] #27
Цитата Сообщение от alex_x_x Посмотреть сообщение
при этом у переменной в регистре отсутствует понятие адреса
Ну это может быть не регистр, а стек. Главное то, что при работе тех кодов, в которых заведомо не будет "всяких прочих" обращений к переменной A, компилятор имеет право построить код, где переменная будет храниться где-то в другом месте. У тебя может быть целая функция, работающая с глобальной переменной, но при этом в начале она один раз прочтётся из памяти на регистр, потом переложится на другой регистр, и в конце с регистра перепишется в память (на своё законное место)

Цитата Сообщение от alex_x_x Посмотреть сообщение
какие сложности испытывает компилятор при оптимизации кода
И какие сложности испытывают те, кто этот код потом читает. Особенно если код компилировался с агрессивными оптимизациями. Особенно если это более качественная архитектура типа IA-64
0
CyBOSSeR
Эксперт С++
2309 / 1682 / 86
Регистрация: 06.03.2009
Сообщений: 3,675
09.03.2012, 20:54 #28
Evg, как уже было сказано выше, компилятор просто удаляет переменную tmp, и в место вызова printf подставляет ее значение. Исправить ситацию можно либо пометив tmp квалификатором volatile, либо так:
C
1
*(&a + (&b - &a)) = 11;
по крайней мере, С++ компилятор на ideone.com стал выдавать ожидаемый ответ после этого.
0
Evg
Эксперт CАвтор FAQ
18920 / 6880 / 504
Регистрация: 30.03.2009
Сообщений: 19,379
Записей в блоге: 30
09.03.2012, 23:56  [ТС] #29
Цитата Сообщение от CyBOSSeR Посмотреть сообщение
Evg, как уже было сказано выше, компилятор просто удаляет переменную tmp, и в место вызова printf подставляет ее значение
А чему равно её значение? Оно равно "b". Т.е. фактически мы имеем два вызова printf'а, в который подаётся "b". Но они печатают разный результат

Цитата Сообщение от CyBOSSeR Посмотреть сообщение
Исправить ситацию можно либо пометив tmp квалификатором volatil
Из каких соображений мы должны помечать как volatile?

Цитата Сообщение от CyBOSSeR Посмотреть сообщение
либо так
Как я уже говорил, компилятор по каким-то причинам выражение "&a + (&b - &a)" НЕ превращают в "&b", хотя по правилам ассоциативности это следует делать
0
CyBOSSeR
Эксперт С++
2309 / 1682 / 86
Регистрация: 06.03.2009
Сообщений: 3,675
10.03.2012, 12:25 #30
Цитата Сообщение от Evg Посмотреть сообщение
А чему равно её значение? Оно равно "b".
А b в свою очередь инициализируется значением 10 и более никак известным компилятору образом не изменяется. Получается, что в любой момент времени tmp имеет значение 10 и поэтому может быть удалена.
Цитата Сообщение от Evg Посмотреть сообщение
Из каких соображений мы должны помечать как volatile?
В данном случае мы имеем дело с ненужными нам оптимизациями переменной, volatile как раз позволяет явно сказать компилятору, что никакие оптимизации нам не нужны.
Цитата Сообщение от Evg Посмотреть сообщение
Как я уже говорил, компилятор по каким-то причинам выражение "&a + (&b - &a)" НЕ превращают в "&b", хотя по правилам ассоциативности это следует делать
Думаю, это от компилятора зависит. На ideone.com, например, если заменить "&a + 1" на "&a + (&b - &a)", то tmp не удаляется.
0
10.03.2012, 12:25
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
10.03.2012, 12:25
Привет! Вот еще темы с ответами:

Указатели и адресная арифметика - C (СИ)
Язык C. Задание звучит так: 2) Для этого фрагмента программы написать значения каждой переменной после каждого шага. Адреса...

Адресная арифметика: поиск max элемента массива - C (СИ)
Здравствуйте! Это программа находит максимальный элемент из введеного массива. Программа работает и все хорошо, если бы не одно но.. Нужно...

Целочисленная арифметика - C (СИ)
Всем добра) Ребята как реализовать эти задачи в рамках одной программы(с использованием простых циклов for и while) 1)Найти наибольшую...

Адрессная арифметика - C (СИ)
Здравствуйте. Не могу понять момент с printf (“Искомая строка начинается с символа %d\n“,istr-str1+1);. strstr в этом случае возвращает...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
30
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru