При каких условиях компилятор MSV C C ++ иногда записывает размер массива непосредственно перед указателем, возвращаемым оператором функции new []? - PullRequest
1 голос
/ 30 мая 2020

Я сейчас работаю над трекером памяти для работы, и мы перегружаем оператор функции new [] во многих его вариантах. При написании некоторых модульных тестов я наткнулся на тот факт, что MSV C C ++ 2019 (с использованием настройки компилятора ISO C ++ 17 Standard (std: c ++ 17)) записывает размер выделенного массива объектов непосредственно перед указателем вернулся к звонилке, но только иногда. Мне не удалось найти никаких задокументированных условий, при которых это произойдет. Может ли кто-нибудь объяснить, что это за условия, как я могу их обнаружить во время выполнения, или указать мне любую документацию?

Чтобы даже определить, что это происходит, мне пришлось дизассемблировать код. Вот C ++:

const size_t k_NumFoos = 6;
Foo* pFoo = new Foo[k_NumFoos];

А вот разборка:

00007FF747BB3683  call        operator new[] (07FF747A00946h)  
00007FF747BB3688  mov         qword ptr [rbp+19E8h],rax  
00007FF747BB368F  cmp         qword ptr [rbp+19E8h],0  
00007FF747BB3697  je          ____C_A_T_C_H____T_E_S_T____0+0FF7h (07FF747BB36F7h)  
00007FF747BB3699  mov         rax,qword ptr [rbp+19E8h]  
00007FF747BB36A0  mov         qword ptr [rax],6  
00007FF747BB36A7  mov         rax,qword ptr [rbp+19E8h]  
00007FF747BB36AE  add         rax,8  
00007FF747BB36B2  mov         qword ptr [rbp+1B58h],rax  

Строки cmp и je взяты из библиотеки Catch2, которую мы используем для наших модульных тестов. . Следующие два mov s, следующие за je, записывают размер массива. Следующие три строки (mov, add, mov) - это место, где он перемещает указатель после того, как он записал размер массива. В основном это все хорошо.

Мы также используем MS VirtualAlloc в качестве внутреннего распределителя для перегруженной функции operator new []. Адрес, возвращаемый из VirtualAlloc, должен быть выровнен для оператора функции new [], который использует std::align_t, и когда выравнивание больше, чем максимальное выравнивание по умолчанию, перемещение указателя в последних трех строках дизассемблирования мешает возвращаемый выровненный адрес. Первоначально я думал, что все выделения, сделанные с помощью оператора функции new [], будут иметь такое поведение. Итак, я протестировал некоторые другие варианты использования оператора функции new [] и обнаружил, что это верно во всех случаях, которые я тестировал. Я написал код для корректировки этого поведения, а затем столкнулся со случаем, когда он не демонстрирует поведение записи размера массива перед возвращенным распределением.

Вот C ++, где он не записывает размер массива до возвращенного выделения:

char **utf8Argv = new char *[ argc ];

argc равен 1. Строка поступает из метода Session::applyCommandLine библиотеки Catch2. Дизассемблирование выглядит так:

00007FF73E189C6A  call        operator new[] (07FF73E07D6D8h)  
00007FF73E189C6F  mov         qword ptr [rbp+168h],rax  
00007FF73E189C76  mov         rax,qword ptr [rbp+168h]  
00007FF73E189C7D  mov         qword ptr [utf8Argv],rax  

Обратите внимание, что после call до operator new[] (07FF73E07D6F8h) нет записи размера массива. Когда я смотрю на два на предмет различий, я вижу, что один пишет в указатель, а другой записывает в указатель на указатель. Однако, насколько мне известно, никакая из этой информации не доступна внутри, во время выполнения, для функции operator new [].

Код здесь взят из Debug | x64 сборка. Есть идеи, как определить, когда это поведение произойдет?

Обновление (для приведенного ниже конво): Class Foo:

template<size_t ArrLen>
class TFoo
{
public:
    TFoo()
    {
        memset(m_bar, 0, ArrLen);
    }
    TFoo(const TFoo<ArrLen>& other)
    {
        strncpy_s(m_bar, other.m_bar, ArrLen);
    }
    TFoo(TFoo<ArrLen>&& victim)
    {
        strncpy_s(m_bar, victim.m_bar, ArrLen);
    }
    ~TFoo()
    {
    }
    TFoo<ArrLen>& operator= (const TFoo<ArrLen>& other)
    {
        strncpy_s(m_bar, other.m_bar, ArrLen);
    }
    TFoo<ArrLen>& operator= (TFoo<ArrLen>&& victim)
    {
        strncpy_s(m_bar, victim.m_bar, ArrLen);
    }

    const char* GetBar()
    {
        return m_bar;
    }
    void SetBar(const char bar[ArrLen])
    {
        strncpy_s(m_bar, bar, ArrLen);
    }

protected:
    char m_bar[ArrLen];
};
using Foo = TFoo<8>;

1 Ответ

0 голосов
/ 30 мая 2020

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

OTOH, для чего-то вроде char * счетчик не требуется, поэтому в качестве небольшой оптимизации ничего не генерируется, или так может показаться.

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

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