Подтвердить что ты не робот

Указатель vs Переменная скорость в С++

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

Я сказал, что время доступа, вероятно, будет таким же, как обычный переменный/идентификатор, является указателем на адрес памяти, в котором хранится значение, точно так же, как указатель. Другими словами, что с точки зрения скорости они оба имеют одинаковую производительность, и что указатели отличаются друг от друга, потому что мы можем указать адрес памяти, на который мы хотим указать.

Интервьюер не казался очень убежденным/удовлетворенным моим ответом (хотя он ничего не сказал, просто продолжал просить что-то еще), поэтому я, хотя и приходил и спрашивал, чтобы мои партнеры были точными, а если нет почему (из теории и технического POV).

4b9b3361

Ответ 1

Переменная не должна находиться в основной памяти. В зависимости от обстоятельств компилятор может хранить его в реестре на протяжении всей или части его жизни, а доступ к регистру намного быстрее, чем доступ к ОЗУ.

Ответ 2

Когда вы получаете доступ к "переменной", вы просматриваете адрес, а затем выбираете значение.

Помните - указатель IS является переменной. Итак, вы:

a) найдите адрес (переменной указателя),

b) выберите значение (адрес, сохраненный в этой переменной)

... и затем...

c) выберите значение по адресу, на который указывает.

Итак, доступ через "указатель" (а не напрямую) включает (немного) дополнительную работу и (немного) более длительное время.

Точно такая же вещь возникает, независимо от того, является ли она переменной указателя (C или С++) или ссылочной переменной (только С++).

Но разница чрезвычайно мала.

Ответ 3

Пусть на мгновение игнорирует оптимизацию и просто подумайте о том, что должна сделать абстрактная машина, чтобы ссылаться на локальную переменную по сравнению с переменной через (локальный) указатель. Если мы имеем локальные переменные, объявленные как:

int i;
int *p;

когда мы ссылаемся на значение i, неоптимизированный код должен уйти, чтобы получить значение, которое (скажем) в 12 после текущего указателя стека, и загрузить его в регистр, чтобы мы могли работать с ним. В то время как когда мы ссылаемся на * p, тот же неоптимизированный код должен получить значение p от 16 до текущего указателя стека, загрузить его в регистр, а затем пойти получить значение, на которое указывает регистр, и загрузить его в другой регистр поэтому мы можем работать с ним, как и раньше. Первая часть работы такая же, но доступ к указателю концептуально включает в себя дополнительный шаг, который необходимо выполнить, прежде чем мы сможем работать со значением.

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

Теперь с оптимизацией может случиться так, что два раза очень близки или идентичны. Верно, что если другой недавний код уже использовал значение p для ссылки на другое значение, вы, возможно, уже найдете p в регистре, так что поиск * p через p выполняется в то же время, что и поиск я через стек указатель. Тем не менее, если вы недавно использовали значение i, вы уже можете найти его в регистре. И хотя то же самое можно сказать и о значении * p, оптимизатор может только повторно использовать его значение из регистра, если он уверен, что p не изменился в среднем. У этой проблемы не возникает повторного использования значения i. Короче говоря, при одновременном доступе к обоим значениям может потребоваться одно и то же время при оптимизации, доступ к локальной переменной почти никогда не будет медленнее (за исключением действительно патологических случаев) и может быть очень быстрым. Это делает правильный ответ на вопрос интервьюера.

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

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

Ответ 4

Какие paulsm4 и LaC сказали + немного asm:

    int y = 0;
mov         dword ptr [y],0  
    y = x;
mov         eax,dword ptr [x]   ; Fetch x to register
mov         dword ptr [y],eax   ; Store it to y
    y = *px;
mov         eax,dword ptr [px]  ; Fetch address of x 
mov         ecx,dword ptr [eax] ; Fetch x 
mov         dword ptr [y],ecx   ; Store it to y

Не то, что, с другой стороны, это имеет большое значение, и это, вероятно, сложнее оптимизировать (например, вы не можете сохранить значение в регистре cpu, поскольку указатель просто указывает на какое-то место в памяти). Таким образом, оптимизированный код для y = x; может выглядеть так:

mov dword ptr [y], ebx - если мы предположим, что локальный var x был сохранен в ebx

Ответ 5

Я думаю, что интервьюер искал, чтобы вы упомянули слово register. Как и в случае, если вы объявляете переменную как регистрационную переменную, компилятор сделает все возможное, чтобы убедиться, что она хранится в регистре в CPU.

Немного поболтать о доступе к шине и согласовании для других типов переменных и указателей помогло бы ее создать.

Ответ 6

Переменная содержит значение определенного типа, и доступ к переменной означает получение этого значения из памяти или из регистра. При получении значения из памяти нам нужно получить его адрес откуда-то - большую часть времени он должен быть загружен в регистр (иногда он может быть частью самой команды загрузки, но это довольно редко).

Указатель сохраняет адрес значения; это значение должно быть в памяти, сам указатель может быть в памяти или в регистре.

Я бы ожидал, что в среднем доступ через указатель будет медленнее, чем доступ к значению через переменную.

Ответ 7

Я думаю, что ключевой частью вопроса является "доступ к переменной". Для меня, если переменная находится в области видимости, зачем вам создавать указатель на нее (или ссылку) для ее доступа? Использование указателя или ссылки будет иметь смысл только в том случае, если переменная сама по себе является какой-либо структурой данных или если вы ее выполняете нестандартным способом (например, интерпретируете int как float).

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

Это будет даже зависеть от ключевого слова. Ключевое слово const может очень хорошо означать, что переменная полностью оптимизирована во время компиляции. Это быстрее, чем указатель. Ключевое слово register не гарантирует, что переменная хранится в регистре. Итак, как вы узнаете, быстрее или нет? Я думаю, что ответ заключается в том, что это зависит от того, что ни один из них не соответствует всем ответам.

Ответ 8

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

Ответ 9

В вашем анализе игнорируется общий сценарий, в котором сам указатель является переменной памяти, к которой также необходимо обращаться.

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

int a = 1234; // luggage combination
int *b = &a;
int **c = &b;
...
int e = a; // one memory access
int e = *b; // two memory accesses
int e = **c; // three memory accesses

Таким образом, короткий ответ на "что быстрее": игнорирование оптимизации компилятора и процессора, которое может произойти, быстрее получить доступ к переменной напрямую.

В наилучшем случае, когда этот код выполняется многократно в узком цикле, значение указателя, вероятно, будет кэшироваться в регистр CPU или, в худшем случае, в кеш процессора L1. В таком случае вполне вероятно, что указатель указателя первого уровня будет таким же быстрым или быстрым, как прямой доступ к переменной, поскольку "непосредственно", вероятно, означает через регистр "указатель стека" (плюс некоторое смещение). В обоих случаях вы используете регистр CPU как указатель на значение.

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

Ответ 10

paulsm4 и LaC уже хорошо объяснили это вместе с другими членами. Я хочу подчеркнуть эффект пейджинга, когда указатель указывает на что-то в куче, которое было выгружено.

= > Локальные переменные доступны либо в стеке, либо в регистре
= > в то время как в случае указателя указатель может указывать на адрес, который не находится в кеше, а пейджинг, безусловно, замедлит скорость.