Por que os compiladores C ++ não otimizam leituras e gravações ausentes para estruturar membros de dados em oposição a variáveis locais distintas?
Estou tentando criar uma matriz local de alguns valores de POD (por exemplo,double
) com fixomax_size
que é conhecido em tempo de compilação e, em seguida, leia um tempo de execuçãosize
valor (size <= max_size
) e processe primeirosize
elementos dessa matriz.
A questão é: por que o compilador não elimina leituras e gravações de pilha quandoarr
esize
são colocados na mesmastruct
/class
, ao contrário do caso em quearr
esize
são variáveis locais independentes?
Aqui está o meu código:
#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]);
}
Saída de montagem paratest_distinct_array_and_size
de Clang com -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
Saída de montagem paratest_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
Os mais recentes compiladores GCC e MSVC fazem basicamente a mesma coisa com as leituras e gravações da pilha.
Como podemos ver, lê e grava noarray_wrapper.size
A variável na pilha não é otimizada no último caso. Há uma gravação desize
valor no local[rsp + 512]
antes do início do loop e uma leitura desse local apóscada iteração.
Então, o compilador espera que queremos modificararray_wrapper.size
deprocess_value(array_wrapper.arr[i])
chamada (pegando o endereço do elemento atual da matriz e aplicando algumas compensações estranhas a ele?)
Mas, se tentássemos fazê-lo a partir dessa ligação, não seria um comportamento indefinido?
Quando reescrevemos o loop da seguinte maneira
for (std::size_t i = 0, sz = array_wrapper.size; i < sz; ++i)
process_value(array_wrapper.arr[i]);
, essas leituras desnecessárias no final de cada iteração desaparecerão. Mas a gravação inicial para[rsp + 512]
permanecerá, o que significa que o compilador ainda espera que possamos acessar oarray_wrapper.size
variável nesse local a partir dessesprocess_value
chamadas (fazendo algumas estranhas mágicas baseadas em deslocamento).
Por quê?
Isso é apenas uma pequena falha nas implementações de compiladores modernos (que esperamos que sejam corrigidos em breve)? Ou o padrão C ++ realmente exige esse comportamento que leva à geração de código menos eficiente sempre que colocamos uma matriz e seu tamanho na mesma classe?
P.S.
Percebo que meu exemplo de código acima pode parecer um pouco artificial. Mas considere o seguinte: eu gostaria de usar umboost::container::static_vector
-modelo de classe no meu código para manipulações "estilo C ++" mais seguras e mais convenientes com matrizes pseudo-dinâmicas de elementos POD. Então meuPODVector
irá conter uma matriz e umsize_t
na mesma classe:
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];
}
};
Uso:
void test_pod_vector(std::size_t size)
{
PODVector<double, max_size> arr(size);
for (double& val : arr)
process_value(val);
}
Se o problema descrito acima é realmente forçado pelo padrão do C ++ (e não é culpa dos escritores do compilador), taisPODVector
nunca será tão eficiente quanto o uso bruto de uma matriz e uma variável "não relacionada" ao tamanho. E isso seria muito ruim para o C ++ como uma linguagem que deseja abstrações sem sobrecarga.