Intereting Posts
C / Assembly: как изменить один бит в регистре CPU? Как избежать «ошибки LNK2005:» (уже определенные функции stdlib) при компиляции libpng с помощью Microsoft Visual Studio ’08? Отключение ASLR в Mac OS X Snow Leopard Не удалось получить данные после подключения, получив ошибку Bad Descriptor Как считать символы в строке юникода в C Каково объяснение «предупреждения: считая, что цикл не бесконечен» Кто обрабатывает C ++ «новый» отказ в распределении памяти? Утечка памяти в собственном коде JNI Почему латентность записи TCP хуже, когда работа чередуется? Массив переменной длины в середине struct – почему этот C-код действителен для gcc Почему утверждается макрос, а не функция? Как я могу выполнить внешние команды в C / Linux без использования системных, popen, fork, exec? Как скомпилировать программу MPI с помощью cmake Печально известное поведение программы pthread Обнаружение присутствия или отсутствия аргументов в макросе C

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

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

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

Вот информативный stream по этой теме: 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) :

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

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

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

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

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

 int factor = 8; 

.. и тогда единственное использование factor переменной состоит в умножении на различные вещи:

 arr[i + factor*j] = arr[i - factor*k]; 

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

Из вышесказанного это должно быть ясно: легко определить, что виртуальный регистр, сопоставленный с 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» не требуется копия регистра. Другой момент: «resultp» присваивается дважды, а поскольку второе присваивание игнорирует предыдущее значение, в коде есть по существу две различные переменные «resultp», и это легко определить путем анализа.

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

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

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

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

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

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

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

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

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


добавление

Я исключил выше, из моего специального определения «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», оптимизатор эффективно разбивает ее на два «locals» hwval.h и hwval.w , а затем определяет, что они эквивалентны h и w - поэтому hwval будет по существу исчезают в коде. Никакие указатели не должны передаваться, никакая функция не изменяет переменные другой функции через указатель; это точно так же, как наличие двух отличных скалярных значений возврата. Это намного проще сделать теперь в C ++ 11 - с помощью std::tie и std::tuple , вы можете использовать этот метод с меньшей детализацией (и без необходимости писать определение структуры).

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

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