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.

questionAnswers(1)

yourAnswerToTheQuestion