Endianness: por que os mesmos bytes viram dois números
Quatro bytes na memória: 12 34 56 78. Leia esses bytes com três APIs diferentes do JavaScript e voltam dois números diferentes. Isso é endianness, e o problema inteiro cabe numa tabela.
| Leitura | Resultado |
|---|---|
new DataView(buf).getUint32(0) | 0x12345678 |
new DataView(buf).getUint32(0, true) | 0x78563412 |
new Uint32Array(buf)[0] | 0x78563412 |
Nada está quebrado e nada lança exceção. Cada chamada aplica uma regra diferente sobre qual ponta de um número de vários bytes vem primeiro.
A pergunta que resolve isso não é “qual é a minha máquina?”, e sim “sob qual convenção esses bytes foram escritos?”. Um PNG no seu disco é big-endian. Um GZIP ao lado dele é little-endian. A sua CPU não vota em nenhum dos dois casos.
Tudo o que vem abaixo foi medido no Node v26.7.0 e no Python 3.14.6, em macOS darwin arm64, onde os.endianness() devolve LE e sys.byteorder devolve little.
1. O que big-endian e little-endian significam de fato
Pegue o valor de 32 bits 0x12345678. São quatro bytes: 12 é o mais significativo, 78 o menos. A endianness decide qual deles cai no endereço mais baixo.
| Disposição | Endereço 0 | Endereço 1 | Endereço 2 | Endereço 3 |
|---|---|---|---|---|
| Big-endian | 12 | 34 | 56 | 78 |
| Little-endian | 78 | 56 | 34 | 12 |
Big-endian guarda a ponta grande primeiro, na mesma ordem em que você escreveria o número no papel. Little-endian guarda a ponta pequena primeiro. Nenhum dos dois mexe nos bits dentro de um byte: 0x12 continua 0x12 nas duas disposições. Só bytes inteiros mudam de lugar.
Se o que atrapalha for a passagem de hexadecimal para binário, o conversor de bases numéricas mostra cada byte em binário ao lado da forma hexadecimal, e o guia de conversão entre binário, hexadecimal e octal trata da notação em si.
1.1 Por que existem duas
A divisão é histórica, não uma questão de princípio. Big-endian se lê do jeito que as pessoas escrevem números, e virou a convenção dos protocolos de rede cedo o bastante para pegar. Little-endian venceu do lado da CPU porque o x86 usa essa ordem e o ARM a adota por padrão. O argumento de eficiência que você encontra repetido em quase todo artigo sobre o assunto está medido na seção 8, onde ele acaba valendo cerca de 2%.
2. Endianness é uma propriedade do formato, não da plataforma
A ordem dos bytes é fixada por quem escreveu os bytes, não pela máquina que os lê. As explicações curtas costumam pular essa parte.
A prova cabe em dois arquivos. Na mesma máquina arm64, dentro do mesmo processo, os dois pedem decodificações opostas.
2.1 PNG é big-endian
A RFC 2083 exige inteiros de vários bytes na ordem de bytes de rede, então comprimentos, larguras e alturas dentro de um PNG são todos big-endian. O layout do cabeçalho é fixo: oito bytes de assinatura, depois um comprimento de chunk de quatro bytes, depois o tipo de chunk com quatro caracteres, depois largura e altura.
const fs = require('node:fs');
const png = fs.readFileSync('public/og/base-converter.png');
png.subarray(0, 8).toString('hex'); // '89504e470d0a1a0a' — assinatura PNG
png.readUInt32BE(8); // 13 — comprimento do chunk IHDR
png.readUInt32BE(16); // 1200 — largura da imagem
png.readUInt32BE(20); // 630 — altura da imagem
png.readUInt32LE(16); // os mesmos quatro bytes, lidos do jeito errado
| Campo | Bytes | Big-endian | Little-endian |
|---|---|---|---|
| Comprimento do IHDR | 00 00 00 0d | 13 | 218.103.808 |
| Largura da imagem | 00 00 04 b0 | 1200 | 2.953.052.160 |
2.2 GZIP é little-endian
A RFC 1952 §2.3.1 é explícita: byte menos significativo primeiro. Os últimos quatro bytes de um fluxo gzip são o ISIZE, o tamanho descomprimido. Comprima 300 bytes de A e confira:
python3 -c "import gzip,sys; sys.stdout.buffer.write(gzip.compress(b'A'*300))" > a.gz
const gz = fs.readFileSync('a.gz');
gz.subarray(-4).toString('hex'); // '2c010000'
gz.readUInt32LE(gz.length - 4); // 300 — correto
gz.readUInt32BE(gz.length - 4); // os mesmos quatro bytes, lidos do jeito errado
| Campo | Bytes | Little-endian | Big-endian |
|---|---|---|---|
| ISIZE do trailer | 2c 01 00 00 | 300 | 738.263.040 |
Mesma máquina, mesmo processo, mesma primitiva de leitura de quatro bytes. Se a sua regra prática é “minha máquina é little-endian, então eu leio little-endian”, um desses dois arquivos vira lixo na decodificação. Quem decide é o formato, toda vez.
3. JavaScript: duas APIs, dois padrões opostos
É aqui que navegador e Node confundem quem lê: as duas formas de enxergar um ArrayBuffer discordam entre si por padrão.
3.1 Endianness no DataView: quem decide é o terceiro argumento
Os métodos de DataView recebem uma flag opcional littleEndian como último argumento. Sem ela, a leitura sai big-endian: setUint32(0, x) e setUint32(0, x, false) são a mesma chamada.
const buf = new ArrayBuffer(4);
new Uint8Array(buf).set([0x12, 0x34, 0x56, 0x78]);
const dv = new DataView(buf);
dv.getUint32(0).toString(16); // '12345678' — big-endian, o padrão
dv.getUint32(0, true).toString(16); // '78563412' — littleEndian: true
A escrita se comporta igual, só que na direção inversa:
const hex = (b) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, '0')).join(' ');
dv.setUint32(0, 0x12345678);
hex(buf); // '12 34 56 78'
dv.setUint32(0, 0x12345678, true);
hex(buf); // '78 56 34 12'
3.2 TypedArray segue a plataforma, e não dá para mudar isso
Uint32Array, Int16Array, Float64Array e os demais usam o que a CPU usar. Não há argumento, opção nem flag de construtor. Nesta máquina arm64 isso significa little-endian, exatamente o oposto do padrão do DataView.
new Uint32Array(buf)[0] = 0x12345678;
hex(buf); // '78 56 34 12'
Ou seja: um único ArrayBuffer lido por new DataView(buf).getUint32(0) e por new Uint32Array(buf)[0] devolve 0x12345678 e 0x78563412. Os dois estão certos. Eles respondem perguntas diferentes.
Visões de um byte são imunes, porque ordem de bytes só existe para unidades maiores que um byte. Uint8Array e Int8Array nunca precisam de flag. Aumente a largura em um passo e ela volta:
const two = new ArrayBuffer(2);
new Uint16Array(two)[0] = 0x00ff;
hex(two); // 'ff 00'
3.3 Buffer do Node: a ordem está no nome do método
O Buffer dispensa padrões e coloca a ordem no nome do método, e é por isso que código Node costuma ser o mais fácil de auditar.
const b = Buffer.from([0x12, 0x34, 0x56, 0x78]);
b.readUInt32BE(0).toString(16); // '12345678'
b.readUInt32LE(0).toString(16); // '78563412'
b.swap32().toString('hex'); // '78563412' — altera b no próprio lugar
O swap32() inverte cada grupo de quatro bytes e devolve o mesmo buffer, não uma cópia. Útil quando você tem um array cheio de inteiros na endianness errada; perigoso se você esqueceu que aquele buffer era compartilhado.
4. struct do Python: cinco prefixos, e o que o @ custa de verdade
4.1 < > ! = @: os cinco prefixos de ordem de bytes do struct pack
Empacotando 0x12345678 como inteiro sem sinal de 32 bits, uma linha por prefixo:
import struct
struct.pack('<I', 0x12345678).hex(' ') # '78 56 34 12' little-endian
struct.pack('>I', 0x12345678).hex(' ') # '12 34 56 78' big-endian
struct.pack('!I', 0x12345678).hex(' ') # '12 34 56 78' ordem de rede
struct.pack('=I', 0x12345678).hex(' ') # '78 56 34 12' ordem nativa, tamanhos padrão
struct.pack('@I', 0x12345678).hex(' ') # '78 56 34 12' ordem nativa, alinhamento nativo
! e > produzem bytes idênticos porque a ordem de bytes de rede é big-endian. O desempacotamento espelha isso, e int.from_bytes dá o mesmo par:
hex(struct.unpack('>I', b'\x12\x34\x56\x78')[0]) # '0x12345678'
hex(struct.unpack('<I', b'\x12\x34\x56\x78')[0]) # '0x78563412'
hex(int.from_bytes(b'\x12\x34\x56\x78', 'big')) # '0x12345678'
hex(int.from_bytes(b'\x12\x34\x56\x78', 'little')) # '0x78563412'
4.2 @ e = diferem no padding, não na ordem dos bytes
Os dois seguem a plataforma, então nesta máquina os dois escrevem em little-endian. A diferença está no alinhamento, e ela muda o tamanho do seu struct:
struct.calcsize('@ci') # 8
struct.calcsize('=ci') # 5
struct.calcsize('<ci') # 5
Um char seguido de um int são cinco bytes de dados. Com @, o padrão quando você não escreve prefixo nenhum, o Python insere três bytes de padding para o int começar num limite de quatro bytes. Com = ou qualquer prefixo explícito de ordem de bytes, o padding some.
Esse é o mecanismo por trás de um bug que parece impossível: alguém acrescenta < para corrigir um problema de ordem de bytes e o tamanho do registro muda por baixo. A ordem dos bytes não teve nada a ver. Sair do @ também desligou o alinhamento nativo, em silêncio.
5. Ordem de bytes de rede, e como outras linguagens escrevem isso
A ordem de bytes de rede é big-endian. Cabeçalhos TCP, UDP e IP carregam seus campos de vários bytes desse jeito, herança da época em que hardware big-endian era comum o bastante para alguém ter de escolher um lado. O C expõe a conversão por htons, htonl, ntohs e ntohl: do host para a rede e de volta, para shorts e longs. Num host little-endian eles trocam os bytes; num host big-endian não fazem nada, e é por isso que código que os omite funciona bem até encontrar outra máquina.
O Go faz o contrário e se recusa a ter qualquer padrão:
import "encoding/binary"
v := binary.BigEndian.Uint32(b)
binary.LittleEndian.PutUint32(b, v)
binary.BigEndian e binary.LittleEndian são valores que você nomeia no ponto da chamada. Não existe caminho dependente da plataforma nem flag opcional para esquecer, o que reduz a revisão de ordem de bytes em Go a ler o identificador.
A mesma disciplina vale para dados em ponto fixo. Uma amostra Q15 ou Q31 é um inteiro comum de 16 ou 32 bits assim que sai do seu código, então ela herda a questão da ordem de bytes junto com todo o resto; o conversor de formato Q mostra o inteiro por trás da fração, e é esse inteiro que é ordenado.
6. Números de ponto flutuante também têm ordem de bytes
Um float não tem nada de especial. O IEEE 754 define o padrão de bits, e depois os mesmos quatro ou oito bytes são dispostos na ordem que o formato exigir.
struct.pack('>f', 1.0).hex(' ') # '3f 80 00 00'
struct.pack('<f', 1.0).hex(' ') # '00 00 80 3f'
struct.pack('>d', 0.1).hex(' ') # '3f b9 99 99 99 99 99 9a'
struct.pack('<d', 0.1).hex(' ') # '9a 99 99 99 99 99 b9 3f'
O conversor IEEE 754 responde à primeira metade da pergunta: digite 3.14159 com FP32 selecionado e você recebe 0x40490FD0. Este artigo responde à segunda metade, que é em que ordem esses quatro bytes chegam ao arquivo. A ferramenta dá o valor; a endianness dá o arranjo.
A linha do double 0.1 também é o motivo de 0.1 + 0.2 se comportar mal. Esses bytes 99 repetidos são uma expansão binária que nunca termina, algo que o guia de precisão de ponto flutuante desmonta.
6.1 O float 1.0 é 3f 80 00 00, ou 00 00 80 3f
1.0 em FP32 é um canário útil porque o padrão de bytes dele é bem desequilibrado. Big-endian escreve 3f 80 00 00; little-endian escreve 00 00 80 3f. Faça o dump de um formato binário desconhecido, ache um campo que você sabe que deveria ser 1.0, e os dois zeros no fim dizem em que ponta você está. Funciona para double também, onde o mesmo valor tem seis bytes zero agrupados de um lado.
7. Ordem de bytes em codificações de texto: o BOM é só uma declaração
UTF-16 e UTF-32 são feitos de unidades de vários bytes, então esbarram exatamente no problema que este artigo descreve. A resposta deles é deixar o arquivo anunciar a própria ordem com uma marca de ordem de bytes, o BOM:
Buffer.from('\uFEFF', 'utf16le').toString('hex'); // 'fffe' — U+FEFF é o BOM
Buffer.from('A', 'utf16le').toString('hex'); // '4100'
fffe no começo significa little-endian; feff significa big-endian. Isso faz do BOM o caso mais comum de um formato declarando a própria ordem de bytes em vez de presumir uma. A história completa, incluindo por que UTF-8 dispensa BOM e o que a marca custa quando aparece sem convite, está no guia de codificação UTF-8, UTF-16 e Unicode.
8. Little-endian é mais rápido? O que dizem 40 milhões de iterações
A afirmação de que little-endian é mais eficiente aparece nos resumos dos buscadores e na maioria das explicações introdutórias sobre o tema. Dá para testar. Quarenta milhões de iterações de uma única leitura de 32 bits nesta máquina arm64:
| Caminho | ns/op |
|---|---|
DataView.getUint32(4, true) (little-endian) | 4.4267 |
DataView.getUint32(4) (big-endian) | 4.5200 |
Buffer.readUInt32LE(4) | 1.5173 |
Buffer.readUInt32BE(4) | 5.0893 |
| Razão | Fator |
|---|---|
| DataView big-endian / little-endian | 1.021× |
| Buffer big-endian / little-endian | 3.354× |
Essas duas razões medem coisas diferentes, e relatar só a segunda seria repetir o erro que os artigos cometem.
O par de DataView é a medição honesta do custo da ordem de bytes. As duas chamadas compilam para o mesmo caminho inline no V8; a big-endian carrega uma instrução ARM REV a mais para trocar os bytes. É esse o 1.021×, cerca de 2%. Pouco, mas não zero: não arredonde para “de graça”.
O par de Buffer mede outra coisa completamente diferente. O V8 tem um caminho rápido dedicado para readUInt32LE que o readUInt32BE não recebe, então o 3.354× é uma diferença de implementação de um runtime, não o preço de trocar bytes numa CPU. Citar esse número como prova de que big-endian é lento seria errado. Troque o runtime e o número muda junto.
Em hardware moderno, converter ordem de bytes sai barato demais para entrar numa discussão de projeto de formato. O argumento histórico de eficiência veio de uma época anterior às instruções dedicadas de troca. Escolha a ordem que o seu protocolo ou os seus vizinhos já usam.
9. Como distinguir um bug de endianness de um bug comum
“Como eu vejo se a minha máquina é big-endian ou little-endian” é os.endianness() no Node e sys.byteorder no Python, uma linha cada, e os buscadores já imprimem a resposta acima dos resultados. Só que é a pergunta errada na maior parte da depuração real: quando você faz o parse de um arquivo ou de um pacote, quem decide é o formato e a sua CPU não entra na conversa.
A habilidade útil é reconhecer o sintoma.
9.1 Dois sintomas: o número absurdo e o número 256×
O barulhento é fácil. Leia a largura do PNG da seção 2 do jeito errado e você recebe 2.953.052.160 para uma imagem de 1200 pixels. Qualquer campo que deveria ser uma contagem modesta e volta na casa dos bilhões é um inteiro de 32 bits invertido até prova em contrário.
O silencioso é o caro. Os bytes 00 00 01 00 são lidos como 256 em big-endian e 65.536 em little-endian. Os dois parecem tamanhos de buffer plausíveis. Nada lança exceção, nenhuma assertion dispara, e o valor está errado por um fator de 256. Bugs assim sobrevivem à revisão de código porque o número na tela parece razoável, o que os coloca na mesma família de um BOM UTF-8 quebrando o parse de um JSON: um detalhe invisível no nível do byte cujo sintoma aponta para o lugar errado, tratado no guia de solução do BOM UTF-8 e erros de parse de JSON.
Duas heurísticas que vale a pena guardar. Valores pequenos com três bytes zero na frente são os que viram em silêncio, porque as duas leituras continuam dentro da faixa. E se inverter os bytes na mão produz um número que faz sentido, você já tem a resposta sem abrir o depurador.
9.2 A ordem em que verificar as coisas
- Verifique primeiro a especificação do formato. A RFC 2083 diz que PNG é big-endian; a RFC 1952 §2.3.1 diz que GZIP é little-endian. O que a sua máquina faz é irrelevante para as duas.
- Verifique em segundo lugar o padrão do seu leitor.
DataView.getUint32(0)é big-endian,Uint32Arrayé a ordem da plataforma,struct.pack('@I', ...)é a ordem da plataforma,binary.BigEndian.Uint32é o que está escrito ali. A maioria dos bugs de ordem de bytes é um terceiro argumento faltando ou um prefixo faltando, não um mal-entendido profundo. - Suspeite da plataforma por último. Ela importa quando você escreve um arquivo com
Uint32Arrayou@e manda esse arquivo para outra arquitetura, e importa quando você compara um dump de memória com uma especificação. Quase nunca importa quando você está lendo um formato bem definido que já disse qual ordem usa.
Perguntas frequentes
Big-endian ou little-endian: qual é melhor?
Nem big-endian nem little-endian é melhor. O correto para cada formato é o que aquele formato especifica, e raramente você escolhe. Em desempenho, uma leitura big-endian de DataView mediu 1.021× a little-endian nesta máquina, cerca de 2%, pequeno demais para guiar qualquer decisão de projeto.
Como eu descubro se a minha máquina é big-endian ou little-endian?
os.endianness() no Node devolve LE aqui, e sys.byteorder no Python devolve little. Os dois são de uma linha. Mas a pergunta importa menos do que parece: quando você faz o parse de um arquivo ou de um pacote, quem dita a ordem dos bytes é o formato e a sua CPU não opina.
DataView e Uint32Array dão números diferentes do mesmo buffer. Isso é bug?
Não, o DataView se comporta como está documentado. DataView.getUint32(0) assume big-endian por padrão, enquanto Uint32Array sempre segue a plataforma, que é little-endian em x86 e no Apple Silicon. Os mesmos bytes, duas convenções. Passe true como terceiro argumento e o DataView concorda.
Por que o meu layout de struct mudou de tamanho quando eu adicionei <?
Porque você saiu do @, o padrão, que preenche para o alinhamento nativo. struct.calcsize('@ci') é 8, enquanto struct.calcsize('=ci') e struct.calcsize('<ci') são 5. Os três bytes de padding antes do int foram embora junto com o alinhamento nativo.
A ordem de bytes de rede é big-endian ou little-endian?
Big-endian. É a convenção que os cabeçalhos TCP/IP usam, e é por isso que htons e htonl existem em C. Em Python, os prefixos ! e > produzem bytes idênticos: struct.pack('!I', 0x12345678) e struct.pack('>I', 0x12345678) dão os dois 12 34 56 78.
A endianness afeta o UTF-8?
Não. UTF-8 é um fluxo de bytes, e cada code point é escrito como uma sequência ordenada de bytes individuais, então não sobra nenhuma unidade de vários bytes para reordenar. UTF-16 e UTF-32 têm esse problema, e é exatamente por isso que carregam um BOM, como visto na seção 7.
Arrays de um byte precisam de algum tratamento de ordem de bytes?
Não. Ordem de bytes só existe para unidades maiores que um byte, então Uint8Array, Int8Array e objetos bytes do Python são imunes. Aumente a largura em um passo e ela volta na hora: new Uint16Array(two)[0] = 0x00ff cai na memória como ff 00 nesta máquina.