Форум программистов
 

Восстановите пароль или Зарегистрируйтесь на форуме, о проблемах и с заказом рекламы пишите сюда - alarforum@yandex.ru, проверяйте папку спам!

Вернуться   Форум программистов > C/C++ программирование > Общие вопросы C/C++
Регистрация

Восстановить пароль
Повторная активизация e-mail

Купить рекламу на форуме - 42 тыс руб за месяц

Ответ
 
Опции темы Поиск в этой теме
Старый 08.04.2024, 10:22   #1
iXNomad
Пользователь
 
Регистрация: 06.01.2021
Сообщений: 45
По умолчанию Уничтожаю миф о том, что ++i эффективнее i++ в цикле for в C++.

Слышал, якобы префиксный инкремент эффективнее, т.к. не создаётся копия объекта.
Я провёл мини-исследование и обнаружил, что для простого цикла for это не так.

Для начала, напишем такую программу в файле increment.cpp:
Код:
#include <iostream>

int main() {
    int x = 155;
    int i = 255;
    for(; i < 260; ++i) {
        ++x;
    }   
    std::cout << x << std::endl;
    for(; i < 265; i++) {
        x++;
    }   
    std::cout << x << std::endl;
    return 0;
}
Эта программа повторяет 2 раза один и тот же цикл, но в первом я использовал префиксные инкременты, а во втором - постфиксные. Числа 155, 255, 260, 265 использованы для дальнейшго удобства чтения ассемблерного кода.

Давайте скомпилируем её при помощи g++. Флаг -S попросит компилятор остановиться перед ассемблированием нашей программы, т.е. мы получим код на языке ассемблера перед преобразованием в чисто машинный код.
Код:
$ g++ increment.cpp -S
Откроем только что созданный компиляторм файл increment.s и посмотрим на нашу функцию main.
Тут фрагмент этого файла. Весь файл нам не нужен. Давайте проанализируем его.
Код:
main:
.LFB1572:
    .cfi_startproc
    pushq   %rbp ; стандартный способ вызова функции - сохраняем в стеке старое значение rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp ; и считаем основой стека место, которое было вершиной стека в момент вызова нашей функции
; для того, чтобы при завершении подпрограммы всё вернуть на место
    .cfi_def_cfa_register 6
; далее происходит "выделение памяти" под две локальные переменные, 
; т.е. сдвиг указателя стека в сторону уменьшения адресов
; (стек на x86 растёт вниз, поэтому отнимаем 2*4 от rsp, 4 байта потому что int занимает 4 байта)
; в итоге -8, и под что g++ надо ещё 8 байт я не знаю
; может выравнивание какое-то, просветите пожалуйста кто знает
    subq    $16, %rsp
; инициализируем x = 155
    movl    $155, -4(%rbp)
; инициализируем i = 255
    movl    $255, -8(%rbp)
; далее происходит наш первый цикл for с префиксными инкрементами
.L3:
    cmpl    $259, -8(%rbp) ; сравниваем i и 259
    jg  .L2 ; если оказалось больше (т.е. 260), то переходим на метку L2, т.е. завершаем цикл)
    addl    $1, -4(%rbp) ; префиксный инкремент ++x
    addl    $1, -8(%rbp) ; префиксный инкремент ++i
    jmp .L3 ; возвращаемся на проверку условия в for
; как видим, каждый инкремент занимает ровно одну машинную инструкцию
.L2:
; дальше происходит что-то непонятно как работающее и непонятно написанное, а так подозреваю
; это системный вызов, который находится в вызываемых функциях
; т.к. у нас в программе был вывод значения переменной в стандартный поток вывода
; следует помнить, что линковки ещё НЕ было, поэтому самого системного вызова в нашем файле нет
; это часть стандартной библиотеки С++
; т.е. когда мы подключаем заголовочный файл, мы именно копипастим заголовочный файл, а не "подключаем библиотеку"
; библиотека подключается только при линковке уже сгенерированного объектного модуля
; мы же остановились на ассемблерном коде, т.к. передали компилятору флаг -S
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    leaq    _ZSt4cout(%rip), %rdi
    call    _ZNSolsEi@PLT
    movq    %rax, %rdx
    movq    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rax
    movq    %rax, %rsi
    movq    %rdx, %rdi
    call    _ZNSolsEPFRSoS_E@PLT
; доходим до нашего второго цикла for и наблюдаем сабж
.L5:
    cmpl    $264, -8(%rbp) ; сравнение
    jg  .L4 ; проверка, если больше 264 (т.е. 265), то выпрыгиваем на .L4 (=из цикла)
    addl    $1, -4(%rbp) ; постфиксный инкремент x++
    addl    $1, -8(%rbp) ; постфиксный инкремент i++
; занимают также, как и префиксный, по одной инструкции
    jmp .L5
.L4:
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    leaq    _ZSt4cout(%rip), %rdi
    call    _ZNSolsEi@PLT
    movq    %rax, %rdx
    movq    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rax
    movq    %rax, %rsi
    movq    %rdx, %rdi
    call    _ZNSolsEPFRSoS_E@PLT
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
Как видим, для компилятора наш код в обоих случаях абсолютно одинаковый и не происходит никакого лишнего копирования.

Далее воспользуемся clang и снова получим increment.s:
Код:
$ clang increment.cpp -S
Естественно код будет другой, потому что другой компилятор.
Обнаружим довольно странные вещи (лишь фрагмент) в наших циклах:
Код:
; clang уменьшает указатель стека аж на 32 байта сразу и проводит манипуляции, которые мне не понятны
     pushq   %rbp
     .cfi_def_cfa_offset 16
     .cfi_offset %rbp, -16
     movq    %rsp, %rbp
     .cfi_def_cfa_register %rbp
     subq    $32, %rsp
     movl    $0, -4(%rbp)
     movl    $155, -8(%rbp) ; тут лежит x
     movl    $255, -12(%rbp) ; тут лежит i
Ну это ладно. Но дальше идут наши два цикла (вызов cout я пропущу):
Код:
.LBB1_1:                                # =>This Inner Loop Header: Depth=1
    cmpl    $260, -12(%rbp)                 # imm = 0x104
    jge .LBB1_4
# %bb.2:                                #   in Loop: Header=BB1_1 Depth=1
    movl    -8(%rbp), %eax ; префиксный инкремент: копируем из стека в регистр
    addl    $1, %eax ; добавляем 1
    movl    %eax, -8(%rbp) ; копируем обратно из регистра в стек
# %bb.3:                                #   in Loop: Header=BB1_1 Depth=1
    movl    -12(%rbp), %eax ; абсолютно то же самое для i
    addl    $1, %eax
    movl    %eax, -12(%rbp)
    jmp .LBB1_1
.LBB1_4:
   ; ... тут вызов cout ...
.LBB1_5:                                # =>This Inner Loop Header: Depth=1
    cmpl    $265, -12(%rbp)                 # imm = 0x109
    jge .LBB1_8
# %bb.6:                                #   in Loop: Header=BB1_5 Depth=1
    movl    -8(%rbp), %eax    ; абсолютно то же самое происходит и для
    addl    $1, %eax               ; постфиксного инкремента
    movl    %eax, -8(%rbp)
# %bb.7:                                #   in Loop: Header=BB1_5 Depth=1
    movl    -12(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -12(%rbp)
    jmp .LBB1_5
.LBB1_8:
   ; ... тут вызов cout ...
Я не знаю, что быстрее, изменить значение ячейки ОЗУ напрямую или скопировать в регистр, увеличить регистр, а потом скопировать обратно. Подозреваю, что g++ тут по идее быстрее.

Вывод: Для итераторов может разница вполне может быть, надо проверить отдельно. Я уверен, что люди это уже исследовали. Но конкретно для простого цикла for с переменной типа int в качестве счётчика разницы НЕТ. Проверил для двух переменных-счётчиков цикла (for (int i = 255, j = 50; ...), я про это) - никакой разницы, ровно одна инструкция. Поэтому можно использовать то, что удобнее и не усложнять.
Приведённый пример работал без флагов -O1, -O2, и -O3. Например, при использовании флага -O2 и -O3 с g++ код превращается в нечто ещё более непонятное, но можно найти заранее вычисленные 160 и 165.
С флагом -O1 вся функция выглядит так, будто компилятор сначала понял, что программа хочет сделать, и потом сгенерировал код, котороый просто выводит нужные числа.
Код:
main:
.LFB1594:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $160, %esi
    leaq    _ZSt4cout(%rip), %rdi
    call    _ZNSolsEi@PLT
    movq    %rax, %rdi
    call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@PLT
    movl    $165, %esi
    leaq    _ZSt4cout(%rip), %rdi
    call    _ZNSolsEi@PLT
    movq    %rax, %rdi
    call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@PLT
    movl    $0, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret 
    .cfi_endproc

Последний раз редактировалось iXNomad; 08.04.2024 в 10:27.
iXNomad вне форума Ответить с цитированием
Старый 08.04.2024, 11:11   #2
p51x
Старожил
 
Регистрация: 15.02.2010
Сообщений: 15,709
По умолчанию

Вы идиот? Компиляторы давно делают эту оптимизацию для простых случаев + кучу других. Хотите сравнить возмите итераторы в дебаге или другие классы с копирование/доп. действиями и т.д.
p51x вне форума Ответить с цитированием
Старый 08.04.2024, 21:17   #3
NetSpace
Участник клуба
 
Аватар для NetSpace
 
Регистрация: 03.06.2009
Сообщений: 1,814
По умолчанию

а где графики, исследования на конкретных расчётных задачах с выводом времени, чтоб этот многострадальный i++ реально протестировать?
Программирование - это единственный способ заставить компьютер делать то, что тебе хочется, а не то, что приходится.
NetSpace вне форума Ответить с цитированием
Старый 09.04.2024, 06:26   #4
Алексей1153
фрилансер
Форумчанин
 
Регистрация: 11.10.2019
Сообщений: 968
По умолчанию

iXNomad, поздравляю с открытием существования оптимизатора

чтобы ещё больше удивиться возможностям оптимизаторов:
Поговорим об оптимизирующих компиляторах
RVO и NRVO в C++17

Цитата:
Сообщение от iXNomad Посмотреть сообщение
Давайте скомпилируем её при помощи g++. Флаг -S попросит компилятор остановиться перед ассемблированием нашей программы, т.е. мы получим код на языке ассемблера перед преобразованием в чисто машинный код.
Код:
к слову, удобно такие эксперименты производить вот тут https://godbolt.org/

Последний раз редактировалось Алексей1153; 09.04.2024 в 07:00.
Алексей1153 вне форума Ответить с цитированием
Старый 10.04.2024, 04:32   #5
Steelcraft
Форумчанин
 
Регистрация: 13.03.2023
Сообщений: 111
По умолчанию

А мне понравилось. Очень напоминает чеховское "Письмо к ученому соседу".
Steelcraft вне форума Ответить с цитированием
Ответ


Купить рекламу на форуме - 42 тыс руб за месяц



Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Как эффективнее обрабатывать большой файл по кускам? ccccfr Win Api 5 24.07.2020 21:22
как вектор стал двухмерным. Почему в первом цикле Layer(), а во втором Matrix(i,j); и зачем во втором цикле функцию back() используют? diomed16 Общие вопросы C/C++ 1 01.07.2020 18:03
что эффективнее? Poma][a Помощь студентам 6 10.11.2011 23:39
Работа в цикле bulldog5293 Общие вопросы Delphi 3 09.08.2011 18:25
Цикл в цикле... Davlet M Помощь студентам 6 25.01.2010 01:42