Могут ли статические локальные переменные сократить время выделения памяти?
Предположим, что у меня есть функция в одной потоковой программе, которая выглядит как
void f(some arguments){
char buffer[32];
some operations on buffer;
}
и f появляется внутри некоторого цикла, который часто вызывает вызов, поэтому я хотел бы сделать это как можно быстрее. Мне кажется, что буфер нужно распределять каждый раз при вызове f, но если я объявляю его статическим, этого не произойдет. Это правильные рассуждения? Это бесплатная скорость? И только из-за этого факта (что легко ускорить), оптимизирующий компилятор уже делает что-то подобное для меня?
Ответы
Ответ 1
Для реализаций, использующих стек для локальных переменных, часто распределение времени связано с продвижением регистра (добавлением к нему значения), таким как регистр Stack Pointer (SP). Это время очень незначительно, обычно одна инструкция или меньше.
Однако инициализация переменных стека занимает немного больше времени, но опять же, немного. Ознакомьтесь с листингом списка ассемблера (сгенерированным компилятором или отладчиком) для получения точных сведений. В стандарте нет ничего о продолжительности или количестве инструкций, необходимых для инициализации переменных.
Распределение статических локальных переменных обычно обрабатывается по-разному. Общий подход состоит в том, чтобы поместить эти переменные в ту же область, что и глобальные переменные. Обычно все переменные в этой области инициализируются перед вызовом main()
. Распределение в этом случае - это назначение адресов в регистры или сохранение информации о зоне в памяти. Здесь не так много времени на трату.
Динамическое распределение - это случай, когда выполняются циклы выполнения. Но это не входит в сферу вашего вопроса.
Ответ 2
Нет, это не бесплатное ускорение.
Во-первых, распределение почти бесплатное для начала (поскольку оно состоит только в добавлении 32 к указателю стека), а во-вторых, существует по меньшей мере две причины, по которым статическая переменная может быть медленнее
- вы теряете локальную сеть. Данные, выделенные в стеке, уже будут в кэше ЦП, поэтому доступ к ней чрезвычайно дешев. Статические данные выделяются в другой области памяти, поэтому их нельзя кэшировать, поэтому это приведет к промаху в кеше, и вам придется ждать сотни тактовых циклов для данных, которые будут извлекаться из основной памяти.
- вы теряете безопасность потока. Если два потока выполняют функцию одновременно, она будет разбиваться и записываться, если только блокировка не помещается, поэтому только один поток за раз разрешает выполнение этого раздела кода. И это будет означать, что вы потеряете преимущество наличия нескольких ядер процессора.
Так что это не бесплатное ускорение. Но возможно, что в вашем случае это быстрее (хотя я сомневаюсь).
Поэтому попробуйте, сравните его и посмотрите, что лучше всего работает в вашем конкретном сценарии.
Ответ 3
Увеличение 32 байтов в стеке практически не будет стоить почти во всех системах. Но вы должны проверить это. Контролируйте статическую версию и локальную версию и отправьте сообщение.
Ответ 4
Как это написано сейчас, для распределения нет затрат: 32 байта находятся в стеке. Единственная реальная работа - вам нужно инициализировать нуль.
Локальная статика здесь не очень хорошая идея. Это не будет быстрее, и ваша функция больше не будет использоваться из нескольких потоков, поскольку все вызовы имеют один и тот же буфер. Не говоря уже о том, что инициализация локальной статики не гарантируется потоком.
Ответ 5
Я бы предположил, что более общий подход к этой проблеме состоит в том, что если у вас есть функция, называемая много раз, которая нуждается в некоторых локальных переменных, тогда рассмотрите ее оболочку в классе и сделайте эти функции-члены переменных. Подумайте, нужно ли было сделать размер динамическим, поэтому вместо char buffer[32]
у вас был std::vector<char> buffer(requiredSize)
. Это дороже, чем массив для инициализации каждый раз через цикл
class BufferMunger {
public:
BufferMunger() {};
void DoFunction(args);
private:
char buffer[32];
};
BufferMunger m;
for (int i=0; i<1000; i++) {
m.DoFunction(arg[i]); // only one allocation of buffer
}
Там также подразумевается создание статического буфера, который заключается в том, что функция теперь небезопасна в многопоточном приложении, поскольку два потока могут вызывать ее и одновременно перезаписывать данные в буфере. С другой стороны, безопасно использовать отдельный BufferMunger
в каждом потоке, который требует его.
Ответ 6
Обратите внимание, что в первом использовании инициализируются переменные уровня static
на уровне С++ (в отличие от C). Это означает, что вы будете вводить стоимость дополнительной проверки времени выполнения. Возможно, филиал может привести к ухудшению производительности, а не к лучшему. (Но на самом деле, вы должны профиль, как упомянули другие.)
Независимо от того, я не думаю, что это того стоит, тем более, что вы намеренно жертвуете повторным вступлением.
Ответ 7
Если вы пишете код для ПК, вряд ли есть какое-либо значимое преимущество в скорости. В некоторых встроенных системах может быть полезно избежать всех локальных переменных. В некоторых других системах локальные переменные могут быть быстрее.
Пример первого: на Z80 код для установки фрейма стека для функции с любыми локальными переменными был довольно длинным. Кроме того, код для доступа к локальным переменным был ограничен использованием режима адресации (IX + d), который был доступен только для 8-битных инструкций. Если X и Y являются глобальными/статическими или обе локальными переменными, утверждение "X = Y" может быть собрано как:
; If both are static or global: 6 bytes; 32 cycles
ld HL,(_Y) ; 16 cycles
ld (_X),HL ; 16 cycles
; If both are local: 12 bytes; 56 cycles
ld E,(IX+_Y) ; 14 cycles
ld D,(IX+_Y+1) ; 14 cycles
ld (IX+_X),D ; 14 cycles
ld (IX+_X+1),E ; 14 cycles
100% штрафного кода и 75% штрафного времени в дополнение к коду и времени для установки кадра стека!
В процессоре ARM одна команда может загрузить переменную, которая находится в пределах +/- 2K указателя адреса. Если функция локальных переменных составляет всего 2 К или меньше, к ним может быть доступна одна инструкция. Глобальные переменные обычно требуют загрузки двух или более инструкций, в зависимости от того, где они хранятся.
Ответ 8
С gcc, я вижу некоторое ускорение:
void f() {
char buffer[4096];
}
int main() {
int i;
for (i = 0; i < 100000000; ++i) {
f();
}
}
И время:
$ time ./a.out
real 0m0.453s
user 0m0.450s
sys 0m0.010s
изменение буфера на статический:
$ time ./a.out
real 0m0.352s
user 0m0.360s
sys 0m0.000s
Ответ 9
В зависимости от того, что именно делает переменная и как ее используют, ускорение почти ничего не значит. Поскольку (на системах x86) стек стека выделяется для всех локальных варов одновременно с простой единичной func (sub esp, amount), таким образом, имея только один другой стек var, исключает любое усиление. единственным исключением из этого является действительно огромные буферы, и в этом случае компилятор может вставить в _chkstk для выделения памяти (но если ваш буфер такой большой, вы должны переоценить свой код). Компилятор не может превратить стекную память в статическую память с помощью оптимизации, так как не может предположить, что функция будет использоваться в одном потоковом окружении, плюс она будет работать с конструкторами объектов и деструкторами и т.д.
Ответ 10
Если в функции есть какие-либо локальные автоматические переменные, необходимо отрегулировать указатель стека. Время, необходимое для корректировки, является постоянным и не будет меняться в зависимости от количества объявленных переменных. Вы можете сэкономить некоторое время, если ваша функция оставлена без каких-либо локальных автоматических переменных.
Если статическая переменная инициализирована, там где-то будет флаг, чтобы определить, была ли уже инициализирована переменная. Проверка флага займет некоторое время. В вашем примере переменная не инициализируется, поэтому эту часть можно игнорировать.
Статические переменные следует избегать, если ваша функция имеет шанс вызвать рекурсивно или из двух разных потоков.
Ответ 11
В большинстве случаев функция будет существенно медленнее. Это связано с тем, что сегмент статических данных не находится рядом с стеком, и вы потеряете согласованность кеша, поэтому при попытке доступа к нему вы получите пропущенный кеш. Однако при распределении регулярного char [32] в стеке он находится рядом со всеми вашими необходимыми данными и очень мало подходит для доступа. Стоимость инициализации массива на основе стека char не имеет смысла.
Это игнорируется, что статика имеет много других проблем.
Вам действительно нужно профилировать свой код и посмотреть, где происходит замедление, потому что ни один профайлер не скажет вам, что выделение буфера символов статического размера является проблемой производительности.