Скалярное `new T` против массива` new T [1] ` - PullRequest
38 голосов
/ 30 сентября 2019

Недавно мы обнаружили, что некоторый код использовал new T[1] систематически (правильно сопоставленный с delete[]), и мне интересно, если это безопасно, или есть некоторые недостатки в сгенерированном коде (в пространстве или времени / производительности)). Конечно, это было скрыто за слоями функций и макросов, но это не относится к делу.

Логически мне кажется, что оба они похожи, но они ли?

Разрешено ли компиляторампревратить этот код (используя литерал 1, а не переменную, а через слои функций, чтобы 1 превращался в переменную аргумента 2 или 3 раза, прежде чем достигнуть кода, используя, таким образом, new T[n]), в скаляр new T?

Какие-либо другие соображения / вещи, которые нужно знать о разнице между этими двумя?

Ответы [ 3 ]

26 голосов
/ 30 сентября 2019

Если T не имеет тривиального деструктора, то для обычных реализаций компилятора new T[1] имеет издержки по сравнению с new T. Версия массива будет выделять немного большую область памяти для хранения количества элементов, поэтому на delete[] он знает, сколько деструкторов должно быть вызвано.

Итак, у него есть издержки:

  • должна быть выделена немного большая область памяти
  • delete[] будет немного медленнее, поскольку для вызова деструкторов требуется цикл, вместо этого вызывается простой деструктор (здесьразница заключается в издержках цикла)

Проверьте эту программу:

#include <cstddef>
#include <iostream>

enum Tag { tag };

char buffer[128];

void *operator new(size_t size, Tag) {
    std::cout<<"single: "<<size<<"\n";
    return buffer;
}
void *operator new[](size_t size, Tag) {
    std::cout<<"array: "<<size<<"\n";
    return buffer;
}

struct A {
    int value;
};

struct B {
    int value;

    ~B() {}
};

int main() {
    new(tag) A;
    new(tag) A[1];
    new(tag) B;
    new(tag) B[1];
}

На моей машине она печатает:

single: 4
array: 4
single: 4
array: 12

Потому что B имеет нетривиальный деструктор, компилятор выделяет дополнительные 8 байтов для хранения количества элементов (поскольку это 64-битная компиляция, для этого требуется 8 дополнительных байтов) для версии массива. Поскольку A выполняет тривиальный деструктор, версия массива A не нуждается в этом дополнительном пространстве.


Примечание: как отмечает Deduplicator, есть небольшое преимущество в производительности при использовании версии массива,если деструктор является виртуальным: в delete[] компилятору не нужно вызывать деструктор виртуально, потому что он знает, что типом является T. Вот простой пример, чтобы продемонстрировать это:

struct Foo {
    virtual ~Foo() { }
};

void fn_single(Foo *f) {
    delete f;
}

void fn_array(Foo *f) {
    delete[] f;
}

Clang оптимизирует этот случай, но GCC не делает: godbolt .

Для fn_single clang испускает проверку nullptr, а затем виртуально вызывает функцию destructor+operator delete. Это должно быть сделано так, как f может указывать на производный тип, у которого есть непустой деструктор.

Для fn_array, clang испускает проверку nullptr, а затем вызывает operator delete, без вызова деструктора, так как он пуст. Здесь компилятор знает, что f фактически указывает на массив Foo объектов, он не может быть производным типом, поэтому он может опускать вызовы пустых деструкторов.

9 голосов
/ 30 сентября 2019

Нет, компилятору не разрешено заменять new T[1] на new T. operator new и operator new[] (и соответствующие удаления) заменяемые ([basic.stc.dynamic] / 2). Определяемая пользователем замена может определить, какой из них вызывается, поэтому правило «как будто» не разрешает эту замену.

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

5 голосов
/ 30 сентября 2019

Правило простое: delete[] должно соответствовать new[] и delete должно соответствовать new: поведение при использовании любой другой комбинации не определено.

Компилятору действительно разрешено поворачиваться new T[1] в простое new T (и соответственно с delete[]), из-за правила как-будто . Хотя я не сталкивался с компилятором, который делает это.

Если у вас есть какие-либо сомнения по поводу производительности, то профилируйте его.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...