Все еще лучше предпочесть pre-increment над post-increment?

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

кажется, что это больше не вызывает серьезной озабоченности (до тех пор, пока встроена), так как мой старый компилятор C++ (GCC 4.4.7), похоже, оптимизирует следующие две функции в идентичный код:

class Int {
    //...
public:
    Int (int x = 0);
    Int & operator ++ ();
    Int operator ++ (int) {
        Int x(*this);
        ++*this;
        return x;
    }
};

Int & test_pre (Int &a) {
    ++a;
    return a;
}

Int & test_post (Int &a) {
    a++;
    return a;
}

результирующая сборка для обоих функции:

    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    movq    %rdi, %rbx
    call    _ZN3IntppEv
    movq    %rbx, %rax
    popq    %rbx
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

если ничего не встроено, однако, кажется, все еще есть преимущество в предпочтении предварительного приращения пост-приращению, так как test_post вынужден обратиться в operator++(int).

предположим operator++(int) встроен как идиоматический конструктор копирования, вызов предварительного приращения и возврат копии, как показано выше. Если конструктор копирования inlined или конструктор копирования по умолчанию реализация, является ли это достаточной информацией для компилятора для оптимизации пост-инкремента, чтобы test_pre и test_post становятся идентичными функциями? Если нет, то какая еще информация требуется?

5 ответов


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

однако для типа класса это мая (если не тут) дело в том, что семантика в этом случае может быть более сложной.

class X { /* code */ };

X x;

++x;
x++; 

последние два вызовы могут быть совершенно разными и могут выполнять разные вещи, как и эти звонки:

x.decrement(); //may be same as ++x (cheating is legal in C++ world!)
x.increment(); //may be same as x++

Так что не позволяйте себе в ловушке для синтаксического сахара.


обычно оператор post-increment в пользовательских типах включает создание копии, которая медленнее и дороже, чем типичный оператор pre-increment.

поэтому оператор pre-increment должен использоваться в предпочтении для пользовательских типов.

также это хороший стиль, чтобы быть последовательный и поэтому pre-increment также должно быть предпочтено со встроенными типами.

пример:

struct test
{
    // faster pre-increment
    test& operator++() // pre-increment
    {
        // update internal state
        return *this; // return this
    }

    // slower post-increment
    test operator++(int)
    {
        test c = (*this); // make a copy
        ++(*this); // pre-increment this object
        return c; // return the un-incremented copy
    }
};

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


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

когда вы пишете заголовок цикла, как

for ( std::size_t i = 0; i < numElements; i++ )

вы не имеете в виду"pls добавить один к значению i, а затем дать мне его старое значение". Вас не волнует возвращаемое значение выражения i++ на всех! Так зачем заставлять компилятор прыгать через обручи и давать одно возвращаемое значение, которое требует наибольшей работы?

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


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

похоже, вы можете привыкнуть к написанию кода, такого как a>b ? a:b вместо использования функции max и оптимизации компиляторы обычно излучают branchless код в таких случаях. Но какой цели он служит, когда мы можем так же легко и, возможно, с большей ясностью написать max(a, b)?

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



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


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

идиоматического встроенного постинкрементного и тривиального конструктора копирования недостаточно для компилятора, чтобы вывести, что две функции test_pre и test_post может быть реализовано идентично. Если деструктор нетривиален, код отличается. Даже с пустым телом деструктора сборка post-increment немного изменяется для рассматриваемого компилятора, GCC 4.4.7:

        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        .cfi_lsda 0x3,.LLSDA1106
        pushq   %rbx
        .cfi_def_cfa_offset 16
        .cfi_offset 3, -16
        movq    %rdi, %rbx
.LEHB0:
        call    _ZN3IntppEv
.LEHE0:
        movq    %rbx, %rax
        popq    %rbx
        .cfi_remember_state
        .cfi_def_cfa_offset 8
        ret
.L12:
        .cfi_restore_state
.L9:
        movq    %rax, %rdi
.LEHB1:
        call    _Unwind_Resume
.LEHE1:
        .cfi_endproc

обратите внимание, что путь выполнения в основном тот же, за исключением некоторых дополнительных .cfi_* операторы, которые не отображаются в версии pre-increment, а также неучтенный вызов _Unwind_Resume. Я считаю, что дополнительный код был добавлен, чтобы справиться с делом деструктор создает исключение. Удаление мертвого кода очистило часть его, так как тело деструктора было пустым, но результат не был идентичным коду с версией pre-increment.