может по-прежнему «знать» индекс другими способами (например, подсчитывать количество его вызовов). Да, это был бы ужасный код. Но семантически это верно без UB, и компилятор должен уважать это.
аюсь создать локальный массив некоторых значений POD (например,double
) с фиксированнойmax_size
что известно во время компиляции, затем прочитайте среду выполненияsize
стоимость (size <= max_size
) и обработать первымsize
элементы из этого массива.
Вопрос в том, почему компилятор не исключает чтение и запись в стек, когдаarr
а такжеsize
помещены в одно и то жеstruct
/class
, в отличие от случая, когдаarr
а такжеsize
такое независимые локальные переменные?
Вот мой код:
#include <cstddef>
constexpr std::size_t max_size = 64;
extern void process_value(double& ref_value);
void test_distinct_array_and_size(std::size_t size)
{
double arr[max_size];
std::size_t arr_size = size;
for (std::size_t i = 0; i < arr_size; ++i)
process_value(arr[i]);
}
void test_array_and_size_in_local_struct(std::size_t size)
{
struct
{
double arr[max_size];
std::size_t size;
} array_wrapper;
array_wrapper.size = size;
for (std::size_t i = 0; i < array_wrapper.size; ++i)
process_value(array_wrapper.arr[i]);
}
Сборочный вывод дляtest_distinct_array_and_size
из Clang с -O3:
test_distinct_array_and_size(unsigned long): # @test_distinct_array_and_size(unsigned long)
push r14
push rbx
sub rsp, 520
mov r14, rdi
test r14, r14
je .LBB0_3
mov rbx, rsp
.LBB0_2: # =>This Inner Loop Header: Depth=1
mov rdi, rbx
call process_value(double&)
add rbx, 8
dec r14
jne .LBB0_2
.LBB0_3:
add rsp, 520
pop rbx
pop r14
ret
Сборочный вывод дляtest_array_and_size_in_local_struct
:
test_array_and_size_in_local_struct(unsigned long): # @test_array_and_size_in_local_struct(unsigned long)
push r14
push rbx
sub rsp, 520
mov qword ptr [rsp + 512], rdi
test rdi, rdi
je .LBB1_3
mov r14, rsp
xor ebx, ebx
.LBB1_2: # =>This Inner Loop Header: Depth=1
mov rdi, r14
call process_value(double&)
inc rbx
add r14, 8
cmp rbx, qword ptr [rsp + 512]
jb .LBB1_2
.LBB1_3:
add rsp, 520
pop rbx
pop r14
ret
Последние компиляторы GCC и MSVC делают в основном то же самое с чтением и записью стека.
Как мы видим, читает и пишет вarray_wrapper.size
переменные в стеке не оптимизируются в последнем случае. Есть запись оsize
ценность в местоположение[rsp + 512]
до начала цикла, и чтение из этого места послекаждый итерация.
Итак, компилятор ожидает, что мы захотим изменитьarray_wrapper.size
изprocess_value(array_wrapper.arr[i])
вызов (взяв адрес текущего элемента массива и применив к нему некоторые странные смещения?)
Но если мы попытаемся сделать это с помощью этого звонка, разве это не будет неопределенным поведением?
Когда мы переписываем цикл следующим образом
for (std::size_t i = 0, sz = array_wrapper.size; i < sz; ++i)
process_value(array_wrapper.arr[i]);
эти ненужные чтения в конце каждой итерации исчезнут. Но начальная запись в[rsp + 512]
останется, это означает, что компилятор все еще ожидает, что мы сможем получить доступ кarray_wrapper.size
переменная в этом месте из этихprocess_value
звонки (делая некоторую странную магию, основанную на смещении).
Почему?
Является ли это небольшим недостатком в реализации современных компиляторов (который, надеюсь, будет исправлен в ближайшее время)? Или стандарт C ++ действительно требует такого поведения, которое приводит к генерации менее эффективного кода всякий раз, когда мы помещаем массив и его размер в один и тот же класс?
Постскриптум
Я понимаю, что мой пример кода выше может показаться немного надуманным. Но учтите это: я хотел бы использовать легкийboost::container::static_vector
-подобный шаблон класса в моем коде для более безопасных и удобных манипуляций в стиле "C ++" с псевдодинамическими массивами элементов POD. Так что мойPODVector
будет содержать массив иsize_t
в том же классе:
template<typename T, std::size_t MaxSize>
class PODVector
{
static_assert(std::is_pod<T>::value, "T must be a POD type");
private:
T _data[MaxSize];
std::size_t _size = 0;
public:
using iterator = T *;
public:
static constexpr std::size_t capacity() noexcept
{
return MaxSize;
}
constexpr PODVector() noexcept = default;
explicit constexpr PODVector(std::size_t initial_size)
: _size(initial_size)
{
assert(initial_size <= capacity());
}
constexpr std::size_t size() const noexcept
{
return _size;
}
constexpr void resize(std::size_t new_size)
{
assert(new_size <= capacity());
_size = new_size;
}
constexpr iterator begin() noexcept
{
return _data;
}
constexpr iterator end() noexcept
{
return _data + _size;
}
constexpr T & operator[](std::size_t position)
{
assert(position < _size);
return _data[position];
}
};
Использование:
void test_pod_vector(std::size_t size)
{
PODVector<double, max_size> arr(size);
for (double& val : arr)
process_value(val);
}
Если описанная выше проблема действительно вызвана стандартом C ++ (и не является ошибкой авторов компиляторов), то такиеPODVector
никогда не будет столь же эффективным, как необработанное использование массива и «не связанной» переменной для размера. И это было бы очень плохо для C ++ как языка, который требует абстракций без накладных расходов.