Existe uma maneira padrão ou alternativa padrão para empacotar uma estrutura em c?

13

Quando a programação no CI considerou inestimável empacotar estruturas usando o __attribute__((__packed__))atributo GCCs, para que eu possa converter facilmente um pedaço estruturado de memória volátil em uma matriz de bytes a serem transmitidos por um barramento, salvos no armazenamento ou aplicados a um bloco de registros. As estruturas empacotadas garantem que, quando tratadas como uma matriz de bytes, não conterão preenchimento, o que é um desperdício, um possível risco à segurança e possivelmente incompatível quando o hardware está em interface.

Não existe um padrão para empacotar estruturas que funcionem em todos os compiladores C? Se não, então sou um discrepante ao pensar que esse é um recurso crítico para a programação de sistemas? Os usuários iniciais da linguagem C não acharam necessário empacotar estruturas ou existe algum tipo de alternativa?

satur9nine
fonte
usar estruturas entre domínios de compilação é uma péssima idéia, principalmente para apontar para o hardware (que é outro domínio de compilação). as estruturas de pacotes são apenas um truque para fazer isso; elas têm muitos efeitos colaterais ruins; portanto, existem muitas outras soluções para seus problemas com menos efeitos colaterais e mais portáteis.
old_timer

Respostas:

12

Em uma estrutura, o que importa é o deslocamento de cada membro do endereço de cada instância da estrutura. Não se trata tanto de como as coisas estão compactadas.

Uma matriz, no entanto, importa como é "empacotada". A regra em C é que cada elemento da matriz é exatamente N bytes do anterior, onde N é o número de bytes usados ​​para armazenar esse tipo.

Mas com uma estrutura, não existe essa necessidade de uniformidade.

Aqui está um exemplo de um esquema de embalagem estranho:

A Freescale (que fabrica microcontroladores automotivos) faz um micro que possui um coprocessador da Time Processing Unit (google para eTPU ou TPU). Possui dois tamanhos de dados nativos, 8 bits e 24 bits, e lida apenas com números inteiros.

Esta estrutura:

struct a
{
  U24 elementA;
  U24 elementB;
};

verá cada U24 armazenado seu próprio bloco de 32 bits, mas apenas na área de endereço mais alta.

Este:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

terá dois U24s armazenados em blocos adjacentes de 32 bits e o U8 será armazenado no "buraco" na frente do primeiro U24 elementA,.

Mas você pode dizer ao compilador para compactar tudo em seu próprio bloco de 32 bits, se desejar; é mais caro na RAM, mas usa menos instruções para acessos.

"empacotar" não significa "empacotar firmemente" - significa apenas algum esquema para organizar elementos de uma estrutura no deslocamento.

Não existe um esquema genérico, ele depende do compilador + da arquitetura.

RichColours
fonte
1
Se o compilador para os reorganiza TPU struct bpara mover elementCantes que qualquer um dos outros elementos, então não é um compilador conformidade C. Elemento rearranjo não é permitido em C
Bart van Ingen Schenau
Interessante, mas o U24 não é um tipo C padrão en.m.wikipedia.org/wiki/C_data_types, portanto, não surpreende que o complacente seja forçado a lidar com isso de uma maneira um tanto estranha.
satur9nine
Ele compartilha a RAM com o núcleo principal da CPU, com tamanho de palavra de 32 bits. Mas este processador possui uma ALU que lida apenas com 24 bits ou 8 bits. Portanto, ele possui um esquema para definir números de 24 bits em palavras de 32 bits. Não padrão, mas um ótimo exemplo de embalagem e alinhamento. Concordado, é muito fora do padrão.
RichColours 15/01/16
6

Quando a programação em CI achou inestimável empacotar estruturas usando GCCs __attribute__((__packed__))[...]

Como você mencionou __attribute__((__packed__)), presumo que sua intenção é eliminar todos os preenchimentos dentro de um struct(faça com que cada membro tenha um alinhamento de 1 byte).

Não existe um padrão para empacotar estruturas que funcionem em todos os compiladores C?

... E a resposta é não". O preenchimento e o alinhamento de dados em relação a uma estrutura (e matrizes contíguas de estruturas na pilha ou pilha) existem por um motivo importante. Em muitas máquinas, o acesso desalinhado à memória pode levar a uma penalidade de desempenho potencialmente significativa (embora diminua em alguns hardwares mais novos). Em alguns casos raros, o acesso desalinhado à memória leva a um erro de barramento irrecuperável (pode até travar todo o sistema operacional).

Como o padrão C é focado na portabilidade, faz pouco sentido ter uma maneira padrão de eliminar todo o preenchimento em uma estrutura e permitir apenas que campos arbitrários sejam desalinhados, pois isso pode potencialmente arriscar tornar o código C não portátil.

A maneira mais segura e portátil de enviar esses dados para uma fonte externa de uma maneira que elimine todo o preenchimento é serializar de / para fluxos de bytes, em vez de apenas tentar enviar o conteúdo da memória não processada do seu structs. Isso também evita que seu programa sofra penalidades de desempenho fora desse contexto de serialização e também permite adicionar livremente novos campos a um structsem desperdiçar e danificar todo o software. Também lhe dará espaço para lidar com endianness e coisas assim, se isso se tornar uma preocupação.

Existe uma maneira de eliminar todo o preenchimento sem alcançar diretivas específicas do compilador, embora isso seja aplicável apenas se a ordem relativa entre os campos não for importante. Dado algo parecido com isto:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

... precisamos do preenchimento para acesso alinhado à memória em relação ao endereço da estrutura que contém esses campos, da seguinte forma:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

... onde .indica preenchimento. Todos xdevem se alinhar a um limite de 8 bytes para desempenho (e às vezes até comportamento correto).

Você pode eliminar o preenchimento de maneira portátil usando uma representação SoA (estrutura da matriz) da seguinte forma (vamos supor que precisamos de 8 Fooinstâncias):

struct Foos
{
   double x[8];
   char y[8];
};

Nós efetivamente demolimos a estrutura. Nesse caso, a representação da memória fica assim:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... e isto:

01234567
yyyyyyyy

... não há mais sobrecarga de preenchimento e sem envolver acesso desalinhado à memória, pois não estamos mais acessando esses campos de dados como um deslocamento de um endereço de estrutura, mas como um deslocamento de um endereço de base para o que é efetivamente uma matriz.

Isso também traz o bônus de ser mais rápido no acesso seqüencial como resultado de menos dados consumidos (preenchimento irrelevante na mistura para diminuir a taxa relevante de consumo de dados da máquina) e também um potencial para o compilador vetorizar o processamento de maneira muito trivial .

A desvantagem é que é uma PITA codificar. Também é potencialmente menos eficiente para acesso aleatório com o passo mais amplo entre os campos, onde frequentemente os representantes de AoS ou AoSoA se saem melhor. Mas essa é uma maneira padrão de eliminar o preenchimento e embalar as coisas o mais firmemente possível, sem estragar o alinhamento de tudo.

ChrisF
fonte
2
Eu argumentaria que ter um meio de especificar explicitamente o layout da estrutura aumentaria enormemente a portabilidade. Embora alguns layouts levem a código muito eficiente em algumas máquinas e código muito ineficiente em outras, o código funcionaria em todas as máquinas e seria eficiente em pelo menos algumas. Por outro lado, na ausência de um recurso desse tipo, a única maneira de fazer o código funcionar em todas as máquinas é provavelmente torná-lo ineficiente em todas as máquinas ou usar várias macros e compilação condicional para combinar uma rápida e não portátil programa e um portátil lento na mesma fonte.
Supercat
Conceitualmente, sim, se pudéssemos especificar tudo, até representações de bits e bytes, requisitos de alinhamento, endianness, etc. e tivéssemos um recurso que permita esse controle explícito em C, enquanto, opcionalmente, o separaremos mais da arquitetura subjacente ... Mas eu estava apenas falando sobre ATM - atualmente a solução mais portátil para um serializador é escrevê-lo de uma maneira que não dependa das representações exatas de bits e bytes e do alinhamento dos tipos de dados. Infelizmente, não temos os meios que o ATM pode fazer de outra maneira eficaz (em C).
5

Nem todas as arquiteturas são iguais, basta ativar a opção de 32 bits em um módulo e ver o que acontece ao usar o mesmo código fonte e o mesmo compilador. A ordem dos bytes é outra limitação bem conhecida. Jogue na representação de ponto flutuante e os problemas pioram. O uso do Packing para enviar dados binários não é portátil. Para padronizá-lo para que ele fosse praticamente utilizável, você precisaria redefinir a especificação da linguagem C.

Embora comum, usar o Pack para enviar dados binários é uma má idéia se você deseja segurança, portabilidade ou longevidade dos dados. Com que frequência você lê um blob binário de uma fonte no seu programa. Com que frequência você verifica se todos os valores são sensatos, se um hacker ou uma alteração de programa não "conseguiu" os dados? No momento em que você codificou uma rotina de verificação, você também pode estar usando rotinas de importação e exportação.

mattnz
fonte
0

Uma alternativa muito comum é "padding nomeado":

struct s {
  short s1;
  char  c2;
  char  reserved; // Padding
};

Isso faz supor a estrutura não será preenchido a 8 bytes.

MSalters
fonte