Может ли gcc / g ++ сказать мне, когда он игнорирует мой регистр? - PullRequest
4 голосов
/ 17 августа 2010

При компиляции кодов 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 будет нормальной переменной.Может ли он сказать мне, действительно ли переменная, которую я использовал, хранится в регистре ЦП?

Ответы [ 5 ]

7 голосов
/ 17 августа 2010

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

7 голосов
/ 17 августа 2010

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

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

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

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

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

3 голосов
/ 17 августа 2010

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

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

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

1 голос
/ 22 февраля 2015

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

0 голосов
/ 17 августа 2010

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

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

...