Этот вопрос, а также некоторые из ответов и несколько других обсуждений ключевых слов «регистр», которые я видел, - похоже, неявно предполагают, что все местные жители сопоставлены либо с конкретным регистром, либо с определенной ячейкой памяти в стек. Это было в целом верно до 15-25 лет назад, и это правда, если вы выключите оптимизацию, но это совсем не так
когда выполняется стандартная оптимизация. Оптимизаторы теперь воспринимают локальные объекты как символические имена, которые вы используете для описания потока данных, а не как значения, которые необходимо хранить в определенных местах.
Примечание: под «местными» здесь я подразумеваю скалярные переменные класса хранения auto (или «register»), которые никогда не используются в качестве операнда «&». Иногда компиляторы также могут разбивать автоструктуры, объединения или массивы на отдельные «локальные» переменные.
Чтобы проиллюстрировать это: предположим, я пишу это в верхней части функции:
int factor = 8;
.. и тогда единственное использование переменной factor
- это умножение на разные вещи:
arr[i + factor*j] = arr[i - factor*k];
В этом случае - попробуйте, если хотите - переменной factor
не будет. Анализ кода покажет, что factor
всегда равно 8, и поэтому все смены превратятся в <<3
. Если бы вы сделали то же самое в 1985 году C, factor
получил бы место в стеке, и было бы многократное использование, поскольку компиляторы в основном работали по одному выражению за раз и ничего не помнили о значениях переменных. Тогда программисты с большей вероятностью использовали бы #define factor 8
для получения лучшего кода в этой ситуации, сохраняя настраиваемый factor
.
Если вы используете -O0
(оптимизация выключена) - вы действительно получите переменную для factor
. Это позволит вам, например, перешагнуть оператор factor=8
, а затем с помощью отладчика изменить factor
на 11 и продолжить работу. Для того чтобы это работало, компилятор не может хранить что-либо в регистрах между операторами, за исключением переменных, которые назначаются конкретным регистрам; и в этом случае отладчик информируется об этом. И он не может попытаться «узнать» что-либо о значениях переменных, поскольку отладчик может их изменить. Другими словами, вам нужна ситуация 1985 года, если вы хотите изменить локальные переменные во время отладки.
Современные компиляторы обычно компилируют функцию следующим образом:
(1) когда локальная переменная назначается более одного раза в функции, компилятор создает разные «версии» переменной, так что каждая из них назначается только в одном месте. Все «чтения» переменной относятся к определенной версии.
(2) Каждому из этих местных жителей присвоен «виртуальный» регистр. Промежуточным результатам расчета также присваиваются переменные / регистры; так
a = b*c + 2*k;
становится чем-то вроде
t1 = b*c;
t2 = 2;
t3 = k*t2;
a = t1 + t3;
(3) Затем компилятор выполняет все эти операции и ищет общие подвыражения и т. Д. Поскольку каждый из новых регистров записывается только один раз, их проще переставить при сохранении корректности. Я даже не начну анализ контуров.
(4) Затем компилятор пытается отобразить все эти виртуальные регистры в фактические регистры для генерации кода. Поскольку каждый виртуальный регистр имеет ограниченное время жизни, можно многократно использовать фактические регистры - «t1» в приведенном выше примере требуется только до сложения, которое генерирует «a», поэтому его можно хранить в том же регистре, что и «a». Когда регистров недостаточно, некоторые из виртуальных регистров могут быть выделены для памяти - или - значение может храниться в определенном регистре, некоторое время сохраняться в памяти и позже загружаться в (возможно) другой регистр , На компьютере с загрузочным хранилищем, где в вычислениях могут использоваться только значения в регистрах, эта вторая стратегия прекрасно справляется с этой задачей.
Из вышесказанного должно быть понятно: легко определить, что виртуальный регистр, сопоставленный с factor
, совпадает с константой '8', и поэтому все умножения на factor
являются умножениями на 8. Даже если factor
изменяется позже, это «новая» переменная, и она не влияет на предыдущее использование factor
.
Еще один вывод, если вы напишите
vara = varb;
.. может быть, а может и нет, что в коде есть соответствующая копия. Например
int *resultp= ...
int acc = arr[0] + arr[1];
int acc0 = acc; // save this for later
int more = func(resultp,3)+ func(resultp,-3);
acc += more; // add some more stuff
if( ...){
resultp = getptr();
resultp[0] = acc0;
resultp[1] = acc;
}
В приведенном выше примере две «версии» acc (начальная и после добавления «more») могут находиться в двух разных регистрах, и тогда «acc0» будет таким же, как и начальный «acc». Поэтому для 'acc0 = acc' копия регистра не понадобится.
Еще один момент: «resultp» присваивается дважды, а поскольку второе присваивание игнорирует предыдущее значение, в коде по существу есть две разные переменные «resultp», и это легко определить с помощью анализа.
Смысл всего этого: не стесняйтесь разбивать сложные выражения на более мелкие, используя дополнительные локальные элементы для промежуточных, если это облегчает выполнение кода. По существу, за это время исполнения штрафа равно нулю, поскольку оптимизатор все равно видит то же самое.
Если вы заинтересованы в получении дополнительной информации, вы можете начать здесь: http://en.wikipedia.org/wiki/Static_single_assignment_form
Смысл этого ответа состоит в том, чтобы (а) дать некоторое представление о том, как работают современные компиляторы, и (б) указать, что при запросе компилятора, если это будет так любезно, поместить конкретную локальную переменную в регистр - на самом деле не имеет смысла. Каждая «переменная» может рассматриваться оптимизатором как несколько переменных, некоторые из которых могут интенсивно использоваться в циклах, а другие нет. Некоторые переменные исчезнут - например, будучи постоянным; или, иногда, переменная temp, используемая в свопе. Или расчеты, которые на самом деле не используются. Компилятор может использовать один и тот же регистр для разных вещей в разных частях кода, в зависимости от того, что на самом деле лучше всего для машины, для которой вы компилируете.
Идея подсказки компилятору о том, какие переменные должны быть в регистрах, предполагает, что каждая локальная переменная отображается в регистр или в область памяти. Это было верно еще тогда, когда Керниган + Ричи разработал язык Си, но это уже не так.
Относительно ограничения, что вы не можете получить адрес переменной регистра: ясно, что нет способа реализовать получение адреса переменной, хранящейся в регистре, но вы можете спросить - так как компилятор может по своему усмотрению игнорировать 'регистрация' - почему это правило действует? Почему компилятор не может просто игнорировать регистр, если я получаю адрес? (как в случае с C ++).
Опять же, вам нужно вернуться к старому компилятору. Исходный компилятор K + R будет анализировать объявление локальной переменной, а затем немедленно решит, назначать ли его регистру или нет (и если да, то какой регистр). Затем он продолжит компилировать выражения, испуская ассемблер для каждого оператора по одному. Если позже выяснилось, что вы берете адрес переменной 'register', которая была назначена регистру, то не было никакого способа справиться с этим, поскольку назначение к тому времени в общем случае было необратимым. Однако было возможно создать сообщение об ошибке и прекратить компиляцию.
В итоге получается, что 'register' по сути устарел:
- компиляторы C ++ игнорируют его полностью
- Компиляторы C игнорируют его, кроме как для принудительного ограничения на
&
и, возможно, не игнорируют его на -O0
, где это может фактически привести к выделению в соответствии с запросом В -O0 вас не беспокоит скорость кода.
Итак, теперь это в основном для обратной совместимости и, вероятно, на основании того, что некоторые реализации все еще могут использовать его для «подсказок».Я никогда не использую его - и пишу код DSP в реальном времени, и провожу немало времени, рассматривая сгенерированный код и находя способы сделать его быстрее.Есть много способов изменить код, чтобы он работал быстрее, и знание работы компиляторов очень полезно.Прошло много времени с тех пор, как я в последний раз обнаружил, что добавление 'register' относится к числу таких способов.
Addendum
Я исключил выше из своего специального определения 'locals'переменные, к которым применяется &
(они, конечно, включены в обычном смысле этого слова).
Рассмотрим приведенный ниже код:
void
somefunc()
{
int h,w;
int i,j;
extern int pitch;
get_hw( &h,&w ); // get shape of array
for( int i = 0; i < h; i++ ){
for( int j = 0; j < w; j++ ){
Arr[i*pitch + j] = generate_func(i,j);
}
}
}
Это может выглядеть совершенно безвредным.Но если вас беспокоит скорость выполнения, учтите следующее: компилятор передает адреса h
и w
на get_hw
, а затем вызывает generate_func
.Давайте предположим, что компилятор ничего не знает о том, что находится в этих функциях (что является общим случаем).Компилятор должен предполагать, что вызов generate_func
может измениться h
или w
.Это совершенно законное использование указателя, переданного в get_hw
- вы можете сохранить его где-нибудь, а затем использовать его позже, пока область, содержащая h,w
, все еще в игре, для чтения или записи этих переменных.
Таким образом, компилятор должен хранить h
и w
в памяти в стеке и не может заранее определить, как долго будет выполняться цикл.Таким образом, некоторые оптимизации будут невозможны, и в результате цикл может быть менее эффективным (в этом примере, во всяком случае, во внутреннем цикле есть вызов функции, поэтому он может не иметь большого значения, но рассмотрим случай, когда есть функциякоторый иногда вызывается во внутреннем цикле, в зависимости от некоторого условия).
Другая проблема здесь в том, что generate_func
может изменить pitch
, и поэтому i*pitch
необходимо выполнить каждыйвремя, а не только когда i
изменяется.
Его можно перекодировать как:
void
somefunc()
{
int h0,w0;
int h,w;
int i,j;
extern int pitch;
int apit = pitch;
get_hw( &h0,&w0 ); // get shape of array
h= h0;
w= w0;
for( int i = 0; i < h; i++ ){
for( int j = 0; j < w; j++ ){
Arr[i*apit + j] = generate_func(i,j);
}
}
}
Теперь переменные apit,h,w
являются «безопасными» локальными в том смысле, как я определил вышеи компилятор может быть уверен, что они не будут изменены никакими вызовами функций.Предполагая, что я не изменяю что-либо в generate_func
, код будет иметь тот же эффект, что и раньше, но мог бы быть более эффективным.
Дженс Гастедт предложил использовать 'register'Ключевое слово как способ пометки ключевых переменных, чтобы запретить использование &
на них, например, другими лицами, поддерживающими код (это не повлияет на сгенерированный код, так как компилятор может определить отсутствие &
без него).Со своей стороны, я всегда тщательно продумываю, прежде чем применять &
к любому локальному скаляру в критической по времени области кода, и, с моей точки зрения, использование 'register' для принудительного применения этого является немного загадочным, но я вижу смысл (к сожалению, это не работает в C ++, так как компилятор просто игнорирует регистр).
Кстати, с точки зрения эффективности кода, лучший способ получить функцию, возвращающую два значения, - это структура:
struct hw { // this is what get_hw returns
int h,w;
};
void
somefunc()
{
int h,w;
int i,j;
struct hw hwval = get_hw(); // get shape of array
h = hwval.h;
w = hwval.w;
...
Это может показаться громоздким (и неудобным для записи),но он будет генерировать более чистый код, чем предыдущие примеры.«Структура hw» будет фактически возвращена в двух регистрах (во всяком случае, в большинстве современных ABI).А благодаря тому, как используется структура hwval, оптимизатор будет эффективно разбивать ее на два «локальных» hwval.h
и hwval.w
, а затем определять, что они эквивалентны h
и w
-поэтому hwval
существенно исчезнет в коде.Нет необходимости передавать указатели, никакая функция не изменяет переменные другой функции через указатель;это как две разные скалярные возвращаемые величины.Это намного проще сделать сейчас в C ++ 11 - с std::tie
и std::tuple
, вы можете использовать этот метод с меньшим количеством слов (и без необходимости писать определение структуры).