Может ли GCC/G ++ сказать мне, когда он игнорирует мой регистр?

StackOverflow https://stackoverflow.com/questions/3500301

Вопрос

При составлении кодов C/C ++ с использованием GCC/G ++, если он игнорирует мой регистр, может ли он мне сказать? Например, в этом коде

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    return 0;
}

J будет использоваться в качестве регистра, но в этом коде

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    int * a = &j;
    return 0;
}

J будет нормальной переменной. Может ли это сказать мне, действительно ли регистр переменной, которую я использовал, хранится в регистре процессора?

Это было полезно?

Решение

Вы можете справедливо предположить, что GCC игнорирует register Ключевое слово, кроме, возможно, в -O0. Анкет Тем не менее, это не должно иметь значения так или иначе, и если вы в такой глубине, вы уже должны читать код сборки.

Вот информативная тема по этой теме: http://gcc.gnu.org/ml/gcc/2010-05/msg00098.html Анкет Еще в старые времена, register Действительно помог компиляторам распределить переменную в регистры, но сегодня распределение регистра может быть выполнено оптимально, автоматически, без подсказок. Ключевое слово продолжает служить двум целям в C:

  1. В C это мешает вам принимать адрес переменной. Поскольку регистры не имеют адресов, это ограничение может помочь простому компилятору C. (Простых компиляторов C ++ не существует.)
  2. А register Объект не может быть объявлен restrict. Анкет Потому что restrict относится к адресам, их пересечение бессмысленно. (C ++ еще нет restrict, и в любом случае, это правило немного тривиально.)

Для C ++ ключевое слово устарело с C ++ 11 и предложен для удаления из стандартного пересмотра, запланированного на 2017 год.

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

Другие советы

Что касается современных методов компиляции и оптимизации, то register Аннотация вообще не имеет никакого смысла. В вашей второй программе вы принимаете адрес j, и регистры не имеют адресов, но одна та же самая локальная или статическая переменная может быть прекрасно храниться в двух разных местах памяти в течение жизни, а иногда и в памяти, а иногда и в реестре или вообще не существует. Действительно, оптимизирующий компилятор будет составлять ваши вложенные петли как ничего, потому что у них нет никаких эффектов и просто назначают свои окончательные значения k а также j. Анкет И затем пропустите эти назначения, потому что оставшийся код не использует эти значения.

Вы не можете получить адрес регистра в C, плюс компилятор может полностью игнорировать вас; Стандарт C99, раздел 6.7.1 (PDF):

Реализация может рассматривать любую декларацию в регистрации просто как автоматическую декларацию. Однако, используется ли на самом деле адресуемое хранилище, адрес какой-либо части объекта, объявленного с помощью реестра спецификатора класса хранения конвертируя имя массива в указатель, как обсуждалось в 6.3.2.1). Таким образом, единственным оператором, который может быть применен к массиву, объявленному с помощью регистров спецификатора класса хранения, является SizeOF.

Если вы не просыпаетесь на 8-битных AVRS или PICS, компилятор, вероятно, будет смеяться над вами, думая, что вы знаете лучше всего и игнорируете свои просьбы. Даже на них я думал, что знаю лучше пару раз и нашел способы обмануть компилятора (с каким -то встроенным ASM), но мой код взорвался, потому что он должен был помассировать кучу других данных, чтобы обойти мое упрямство.

Этот вопрос и некоторые ответы, а также несколько других обсуждений ключевых слов «регистрации», которые я видел - кажется, неявно предполагают, что все местные жители отображаются либо с определенным регистром, либо с определенным местоположением памяти в стеке. Как правило, это было верно до 15-25 лет назад, и это правда, если вы отключите оптимизацию, но это совсем не так, когда выполняется стандартная оптимизация. В настоящее время оптимизаторы рассматривают местные жители как символические имена, которые вы используете для описания потока данных, а не как значения, которые необходимо хранить в определенных местах.

Примечание. Под «местными жителями» здесь я имею в виду: скалярные переменные, класса класса хранения (или «регистр»), которые никогда не используются в качестве операнда «&». Компиляторы иногда могут разбивать автострады, профсоюзы или массивы в отдельные «локальные» переменные.

Чтобы проиллюстрировать это: предположим, я пишу это вверху функции:

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 (начальные и после добавления «больше») могут быть в двух разных регистрах, а «ACC0» будет таким же, как и inital «ACC». Таким образом, для «ACC0 = ACC» не потребуется копия регистра. Другой момент: «Результат» назначается дважды, и, поскольку второе назначение игнорирует предыдущее значение, в коде существует по существу две различные переменные «результат», и это легко определять анализом.

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

Если вы заинтересованы в изучении большего, вы можете начать здесь: http://en.wikipedia.org/wiki/static_single_assignment_form

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

Понятие намекает компилятора относительно того, какие переменные должны находиться в регистрах, предполагает, что каждая локальная переменная отображает в регистр или в местоположении памяти. Это было правдой, когда Kernighan + Ritchie разработал язык C, но больше не правда.

Что касается ограничения, которое вы не можете взять на адрес переменной реестра: ясно, что нет способа реализовать адрес переменной, хранящейся в реестре, но вы можете спросить - поскольку компилятор имеет право игнорировать «регистр» - Почему это правило на месте? Почему компилятор не может просто игнорировать «регистр», если мне случится взять адрес? (как в случае C ++).

Опять же, вы должны вернуться к старому компилятору. Первоначальный компилятор K+R проанализирует локальную переменную объявление, а затем немедленно Решите, назначать ли его регистр или нет (и если да, то регистр). Затем он будет продолжать скомпилировать выражения, излучая ассемблер для каждого заявления по одному. Если позже он обнаружил, что вы взяли адрес переменной «регистра», которая была назначена в реестр, не было никакого способа справиться с этим, поскольку в целом назначение было необратимым к тому времени. Однако было возможно сгенерировать сообщение об ошибке и прекратить компиляцию.

Итог, похоже, что «регистр» по сути устарел:

  • Компиляторы C ++ полностью игнорируют это
  • C Компиляторы игнорируют это, кроме как обеспечить ограничение на & - и, возможно, не игнорируйте это в -O0 где это может на самом деле привести к распределению по запросу. В -О0 вы не обеспокоены скоростью кода.

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


Приложение

Я исключил выше, из моего особого определения «местных жителей», переменных, к которым & применяется (это, конечно, включены в обычное смысл термина).

Рассмотрим код ниже:

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, Код будет иметь тот же эффект, что и раньше, но может быть более эффективным.

Jens Gustedt предложил использовать ключевое слово «регистрации» в качестве способа помечения переменных ключей, чтобы препятствовать использованию & На них, например, другие, поддерживающие код (он не повлияет на сгенерированный код, поскольку компилятор может определить отсутствие & без этого). Со своей стороны, я всегда тщательно думаю, прежде чем подавать заявление & на любой локальный скаляр в критической области кода, и, по моему мнению, используя «регистр» для обеспечения соблюдения этого немного загадочного, но я вижу смысл (к сожалению, он не работает в 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;
    ...

Это может выглядеть громоздко (и это является громоздким писать), но он будет генерировать более чистый код, чем предыдущие примеры. «Struct HW» фактически будет возвращен в двух регистрах (в любом случае на большинстве современных ABI). И из -за того, как используется структура «hwval», оптимизатор эффективно разбивает его на двух «местных жителей» hwval.h а также hwval.w, а затем определите, что они эквивалентны h а также w -- так hwval по существу исчезнет в коде. Никакие указатели не должны быть переданы, никакая функция не изменяет переменные другой функции с помощью указателя; Это все равно, что иметь два различных скалярных возвратных значений. Это намного проще сделать сейчас в C ++ 11 - с std::tie а также std::tuple, вы можете использовать этот метод с меньшей многословностью (и без необходимости писать определение структуры).

Ваш второй пример недействителен в C. Так что вы хорошо видите, что register Ключевое слово что -то меняет (в C). Это только для этой цели, чтобы препятствовать принятию адреса переменной. Так что просто не принимайте свое название «Зарегистрироваться» в устной форме, это неправильно, но придерживайтесь его определения.

Что C ++, кажется, игнорирует register, Ну, у них должна быть причина для этого, но я считаю, что это немного грустно найти одну из этих тонких разниц, когда действительный код для одного является недействительным для другого.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top