Skip to content
Kembali ke Blog
Tutorial

Endianness: Mengapa Byte yang Sama Jadi Dua Angka Berbeda

Empat byte yang sama, 12 34 56 78, terbaca 0x12345678 atau 0x78563412 tergantung pembacanya. Byte order di JavaScript, Python, Go, PNG, GZIP — diukur online.

13 menit membaca

Endianness: Mengapa Byte yang Sama Jadi Dua Angka Berbeda

Empat byte duduk di memori: 12 34 56 78. Baca lewat tiga API JavaScript yang berbeda, dan angka yang keluar ada dua. Itulah endianness, dan seluruh persoalannya muat dalam satu tabel.

Cara bacaHasil
new DataView(buf).getUint32(0)0x12345678
new DataView(buf).getUint32(0, true)0x78563412
new Uint32Array(buf)[0]0x78563412

Tidak ada yang rusak dan tidak ada yang melempar error. Tiap pemanggilan memakai aturan berbeda soal ujung mana dari angka multi-byte itu yang datang lebih dulu.

Satu pergeseran sudut pandang menghemat banyak waktu: endianness bukan pertanyaan “mesin saya yang mana?”, melainkan “byte ini ditulis mengikuti konvensi siapa?” Berkas PNG di disk Anda big-endian. Berkas GZIP di sebelahnya little-endian. CPU Anda tidak ikut menentukan keduanya.

Semua yang ada di bawah ini diukur pada Node v26.7.0 dan Python 3.14.6 di macOS darwin arm64, tempat os.endianness() mengembalikan LE dan sys.byteorder mengembalikan little.

1. Apa arti big-endian dan little-endian sebenarnya

Ambil nilai 32-bit 0x12345678. Nilai itu terdiri dari empat byte: 12 yang paling signifikan, 78 yang paling tidak signifikan. Endianness menentukan byte mana di antara keempatnya yang mendarat di alamat terendah.

SusunanAlamat 0Alamat 1Alamat 2Alamat 3
Big-endian12345678
Little-endian78563412

Big-endian menaruh ujung besar lebih dulu, urutan yang sama seperti waktu Anda menulis angka itu di kertas. Little-endian menaruh ujung kecil lebih dulu. Keduanya sama sekali tidak menyentuh bit di dalam satu byte: 0x12 tetap 0x12 pada kedua susunan. Yang berpindah hanya byte utuh.

Kalau langkah dari heksa ke biner masih terasa goyah, konverter basis bilangan menampilkan tiap byte dalam bentuk biner berdampingan dengan bentuk heksanya, dan panduan konversi biner, heksadesimal, dan oktal membahas notasinya sendiri.

1.1 Kenapa jumlahnya ada dua

Pembelahannya lahir dari sejarah, bukan dari prinsip. Big-endian terbaca seperti cara orang menulis angka, dan konvensi itu masuk ke protokol jaringan cukup awal sehingga bertahan sampai sekarang. Little-endian menang di sisi CPU karena x86 memakainya dan ARM menjadikannya default. Argumen efisiensi yang muncul berulang-ulang di hampir tiap artikel soal topik ini diukur di bagian 8; nilainya sekitar 2%.

2. Endianness adalah properti format, bukan properti platform

Siapa pun yang menulis byte itu yang menetapkan urutannya, bukan mesin yang membacanya. Penjelasan singkat hampir selalu melompati hal ini.

Buktinya cukup satu laptop dan dua berkas. Di mesin arm64 yang sama, di dalam proses yang sama, keduanya menuntut dekode yang berlawanan.

2.1 PNG adalah big-endian

RFC 2083 mewajibkan bilangan bulat multi-byte dalam network byte order, jadi tiap panjang, lebar, dan tinggi di dalam PNG bersifat big-endian. Tata letak header-nya sudah pasti: delapan byte tanda tangan, lalu panjang chunk empat byte, lalu tipe chunk empat karakter, lalu lebar dan tinggi.

const fs = require('node:fs');
const png = fs.readFileSync('public/og/base-converter.png');

png.subarray(0, 8).toString('hex'); // '89504e470d0a1a0a' — PNG signature
png.readUInt32BE(8);                // 13   — IHDR chunk length
png.readUInt32BE(16);               // 1200 — image width
png.readUInt32BE(20);               // 630  — image height

png.readUInt32LE(16);               // the same four bytes, read the wrong way
FieldByteBig-endianLittle-endian
Panjang IHDR00 00 00 0d13218,103,808
Lebar gambar00 00 04 b012002,953,052,160

2.2 GZIP adalah little-endian

RFC 1952 §2.3.1 mengejanya terang-terangan: byte paling tidak signifikan lebih dulu. Empat byte terakhir sebuah stream gzip adalah ISIZE, yaitu ukuran sebelum kompresi. Kompres 300 byte berisi A lalu periksa:

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 — correct
gz.readUInt32BE(gz.length - 4);    // the same four bytes, read the wrong way
FieldByteLittle-endianBig-endian
ISIZE trailer2c 01 00 00300738,263,040

Mesin yang sama, proses yang sama, primitif baca empat byte yang sama. Kalau aturan kerja Anda berbunyi “mesin saya little-endian, jadi saya baca little-endian”, satu dari dua berkas ini pasti terdekode jadi sampah. Formatnya yang memutuskan, setiap kali.

3. JavaScript: dua API, dua default yang berlawanan

Di sinilah browser dan Node jadi membingungkan: dua cara memandang ArrayBuffer yang sama justru saling bertolak belakang secara default.

3.1 Endianness DataView: argumen ketiga yang menentukan

Metode DataView menerima flag opsional littleEndian sebagai argumen terakhir. Hilangkan flag itu, Anda dapat big-endian. setUint32(0, x) dan setUint32(0, x, false) adalah pemanggilan yang persis sama.

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, the default
dv.getUint32(0, true).toString(16); // '78563412'  — littleEndian: true

Penulisan berperilaku serupa, hanya terbalik arahnya:

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 mengikuti platform, dan Anda tidak bisa mengubahnya

Uint32Array, Int16Array, Float64Array, dan kerabatnya memakai apa pun yang dipakai CPU. Tidak ada argumen atau flag konstruktor yang bisa mengubahnya. Di mesin arm64 ini artinya little-endian, persis kebalikan dari default DataView.

new Uint32Array(buf)[0] = 0x12345678;
hex(buf); // '78 56 34 12'

Jadi satu ArrayBuffer yang dibaca lewat new DataView(buf).getUint32(0) dan lewat new Uint32Array(buf)[0] menghasilkan 0x12345678 dan 0x78563412. Keduanya benar, karena masing-masing menjawab pertanyaan yang berbeda.

View selebar satu byte kebal terhadap soal ini, sebab urutan byte hanya ada untuk satuan yang lebih lebar dari satu byte. Uint8Array dan Int8Array tidak pernah butuh flag. Lebarkan satu tingkat saja, soal itu langsung balik lagi:

const two = new ArrayBuffer(2);
new Uint16Array(two)[0] = 0x00ff;
hex(two); // 'ff 00'

3.3 Node Buffer: sebutkan urutannya di nama metode

Buffer tidak punya default sama sekali; urutannya ada di nama metode. Itu sebabnya kode Node biasanya paling gampang diaudit.

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' — mutates b in place

swap32() membalik tiap kelompok empat byte lalu mengembalikan buffer yang sama, bukan salinannya. Praktis kalau Anda memegang sederet bilangan bulat yang endian-nya salah; berbahaya kalau Anda lupa buffer itu dipakai bersama.

4. struct di Python: lima prefiks, dan berapa harga @ sebenarnya

4.1 < > ! = @: lima prefiks struct pack byte order

Mengemas 0x12345678 sebagai bilangan bulat 32-bit tanpa tanda, satu baris untuk tiap prefiks:

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'  network order
struct.pack('=I', 0x12345678).hex(' ')  # '78 56 34 12'  native order, standard sizes
struct.pack('@I', 0x12345678).hex(' ')  # '78 56 34 12'  native order, native alignment

! dan > menghasilkan byte yang identik karena network byte order memang big-endian. Pembongkarannya mencerminkan hal itu, dan int.from_bytes memberi pasangan yang sama:

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 @ dan = berbeda di padding, bukan di byte order

Keduanya mengikuti platform, jadi di mesin ini keduanya sama-sama menulis little-endian. Bedanya ada di alignment (penyelarasan), dan itu mengubah ukuran struct Anda:

struct.calcsize('@ci')  # 8
struct.calcsize('=ci')  # 5
struct.calcsize('<ci')  # 5

Sebuah char yang diikuti sebuah int berisi lima byte data. Di bawah @, yaitu default ketika Anda tidak menulis prefiks sama sekali, Python menyelipkan tiga byte padding supaya int mulai tepat di batas empat byte. Di bawah = atau prefiks byte order eksplisit mana pun, padding itu lenyap.

Itulah mekanisme di balik bug yang kedengarannya mustahil: seseorang menambahkan < untuk membereskan masalah byte order, lalu panjang record-nya ikut berubah tanpa ada yang menyentuhnya. Byte order sama sekali bukan penyebabnya. Beralih dari @ diam-diam mematikan native alignment sekalian.

5. Network byte order, dan cara bahasa lain menuliskannya

Network byte order adalah big-endian. Header TCP, UDP, dan IP membawa field multi-byte mereka dengan cara itu, warisan dari masa ketika perangkat keras big-endian masih cukup umum sehingga seseorang harus memilih salah satu sisi. C menyediakan konversinya lewat htons, htonl, ntohs, dan ntohl: host ke network dan sebaliknya, untuk short dan long. Di host little-endian keempatnya menukar byte; di host big-endian keempatnya tidak melakukan apa pun, dan itulah alasan kode yang melewatkannya tetap jalan mulus sampai bertemu mesin lain.

Go mengambil pendekatan berlawanan dan menolak punya default sama sekali:

import "encoding/binary"

v := binary.BigEndian.Uint32(b)
binary.LittleEndian.PutUint32(b, v)

binary.BigEndian dan binary.LittleEndian adalah nilai yang Anda sebutkan langsung di tempat pemanggilan. Tidak ada jalur yang bergantung platform, dan tidak ada flag opsional yang bisa terlupa. Meninjau byte order di Go cukup dengan membaca identifier-nya.

Disiplin yang sama berlaku untuk data fixed-point. Sampel Q15 atau Q31 hanyalah bilangan bulat 16- atau 32-bit begitu ia keluar dari kode Anda, jadi ia ikut mewarisi pertanyaan byte order; konverter format Q menampilkan bilangan bulat di balik pecahannya, dan bilangan bulat itulah yang diurutkan.

6. Bilangan floating-point juga punya urutan byte

Sebuah float tidak istimewa. IEEE 754 mendefinisikan pola bit-nya, lalu formatnya yang menentukan dalam urutan apa empat atau delapan byte itu ditulis.

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'

Konverter IEEE 754 menjawab separuh pertama pertanyaannya: ketik 3.14159 dengan FP32 terpilih dan Anda dapat 0x40490FD0. Separuh keduanya ada di sini, yaitu dalam urutan apa keempat byte itu sampai ke berkas.

Baris double 0.1 juga sekaligus alasan 0.1 + 0.2 berulah. Deretan byte 99 yang berulang itu adalah ekspansi biner yang tidak pernah berhenti, dan panduan presisi floating-point membedahnya.

6.1 float 1.0 adalah 3f 80 00 00, atau 00 00 80 3f

1.0 dalam FP32 adalah penanda pengenal yang praktis karena pola byte-nya berat sebelah. Big-endian menulis 3f 80 00 00; little-endian menulis 00 00 80 3f. Bongkar sebuah format biner yang asing, cari field yang Anda tahu seharusnya bernilai 1.0, dan dua byte nol di ekornya langsung memberi tahu Anda sedang berada di ujung yang mana. Cara ini juga jalan untuk double, yang nilainya punya enam byte nol berkumpul di satu sisi.

7. Urutan byte di pengodean teks: BOM hanyalah sebuah deklarasi

UTF-16 dan UTF-32 tersusun dari satuan multi-byte, jadi keduanya menabrak persis masalah yang dibahas artikel ini. Jawaban mereka adalah membiarkan berkas mengumumkan urutannya sendiri lewat byte order mark:

Buffer.from('\uFEFF', 'utf16le').toString('hex'); // 'fffe' — U+FEFF is the BOM
Buffer.from('A', 'utf16le').toString('hex');      // '4100'

fffe di depan berarti little-endian; feff berarti big-endian. BOM adalah contoh paling umum dari format yang mendeklarasikan urutan byte-nya sendiri alih-alih mengasumsikannya. Cerita lengkapnya, termasuk kenapa UTF-8 tidak butuh BOM dan apa harganya ketika tanda itu muncul tanpa diundang, ada di panduan pengodean UTF-8, UTF-16, dan Unicode.

8. Apakah little-endian lebih cepat? Ini kata 40 juta iterasi

Klaim bahwa little-endian lebih efisien muncul di ringkasan mesin pencari dan di hampir semua penjelasan pengantar soal topik ini. Klaim itu bisa diuji. Empat puluh juta iterasi satu operasi baca 32-bit di mesin arm64 ini:

Jalurns/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
RasioFaktor
DataView big-endian / little-endian1.021×
Buffer big-endian / little-endian3.354×

Kedua rasio itu mengukur hal yang berbeda, dan melaporkan hanya yang kedua sama saja dengan mengulang kekeliruan artikel-artikel tadi.

Pasangan DataView adalah pengukuran yang jujur untuk biaya byte order. Kedua pemanggilan terkompilasi ke jalur inline yang sama di V8; versi big-endian membawa satu instruksi ARM REV tambahan untuk menukar byte-nya. Itulah angka 1.021×, kira-kira 2%. Kecil, tetapi bukan nol; jangan bulatkan jadi “gratis”.

Pasangan Buffer mengukur sesuatu yang sama sekali lain. V8 punya jalur cepat khusus untuk readUInt32LE yang tidak diberikan ke readUInt32BE, jadi angka 3.354× itu adalah perbedaan implementasi di satu runtime, bukan harga menukar byte di CPU. Mengutipnya sebagai bukti bahwa big-endian lambat jelas keliru. Ganti runtime-nya, angkanya ikut berubah.

Kesimpulan yang memang didukung angka-angka ini: di perangkat keras modern, konversi urutan byte terlalu murah untuk masuk ke diskusi desain format. Argumen efisiensi historis itu lahir dari era sebelum instruksi swap khusus ada. Pilih urutan yang sudah ditetapkan protokol Anda, atau yang dipakai sistem di seberang.

9. Cara membedakan bug endianness dari bug biasa

Ada satu pertanyaan yang sengaja tidak dibahas panjang di sini. “Bagaimana cara memeriksa apakah mesin saya big-endian atau little-endian” cukup dijawab os.endianness() di Node dan sys.byteorder di Python, masing-masing satu baris, dan mesin pencari sudah mencetaknya di atas daftar hasil. Pertanyaan itu juga keliru untuk sebagian besar sesi debugging nyata: ketika Anda mengurai berkas atau paket, formatnya yang memutuskan dan CPU Anda tidak ikut campur. Yang lebih berguna adalah mengenali gejalanya.

9.1 Dua gejala: angka absurd dan angka 256×

Yang berisik gampang dikenali. Baca lebar PNG dari bagian 2 dengan cara yang salah, Anda dapat 2,953,052,160 untuk gambar selebar 1200 piksel. Field mana pun yang seharusnya berisi hitungan sederhana tetapi kembali dalam angka miliaran adalah bilangan bulat 32-bit yang terbalik sampai terbukti sebaliknya.

Yang senyap justru yang mahal. Byte 00 00 01 00 terbaca 256 sebagai big-endian dan 65,536 sebagai little-endian. Keduanya tampak seperti ukuran buffer yang masuk akal. Tidak ada error yang dilempar dan tidak ada assertion yang menyala, sementara nilainya meleset 256 kali lipat. Bug semacam ini lolos dari code review karena angka di layar terlihat wajar, dan itu menempatkannya sekeluarga dengan BOM UTF-8 yang merusak parsing JSON: detail level byte yang tak kasatmata dengan permukaan error yang menyesatkan, dibahas di panduan mengatasi BOM UTF-8 yang merusak parsing JSON.

Nilai kecil dengan tiga byte nol di depan adalah yang paling sering berbalik diam-diam, sebab kedua pembacaannya sama-sama masih masuk rentang wajar. Dan kalau membalik byte-nya dengan tangan menghasilkan angka yang masuk akal, jawabannya sudah di tangan Anda tanpa perlu menyentuh debugger.

9.2 Urutan memeriksanya

  1. Periksa spesifikasi formatnya lebih dulu. RFC 2083 menyatakan PNG big-endian; RFC 1952 §2.3.1 menyatakan GZIP little-endian. Apa pun yang dilakukan mesin Anda tidak relevan bagi keduanya.
  2. Periksa default pembacanya di langkah kedua. DataView.getUint32(0) big-endian, Uint32Array mengikuti urutan platform, struct.pack('@I', ...) mengikuti urutan platform, binary.BigEndian.Uint32 sesuai bunyi namanya. Kebanyakan bug byte order berupa argumen ketiga yang hilang atau prefiks yang hilang, bukan salah paham yang mendalam.
  3. Curigai platform paling belakangan. Platform baru berpengaruh saat Anda menulis berkas dengan Uint32Array atau @ lalu mengirimkannya ke arsitektur lain, dan saat Anda mengadu dump memori dengan spesifikasi. Platform hampir tidak pernah berpengaruh saat Anda membaca format yang terdefinisi rapi dan sudah menyebutkan urutan yang dipakainya.

Pertanyaan yang sering diajukan

Mana yang lebih baik, big-endian atau little-endian?

Baik big-endian maupun little-endian tidak ada yang lebih baik. Yang benar adalah apa pun yang ditetapkan formatnya, dan Anda jarang punya kesempatan memilih. Soal performa, pembacaan DataView big-endian terukur 1.021× dari versi little-endian di mesin ini, kira-kira 2%, jauh terlalu kecil untuk mendorong keputusan desain apa pun.

Bagaimana cara tahu mesin saya big-endian atau little-endian?

os.endianness() di Node mengembalikan LE di sini, dan sys.byteorder di Python mengembalikan little. Keduanya cukup satu baris. Pertanyaannya sendiri tidak sepenting kelihatannya: ketika Anda mengurai berkas atau paket, formatnya yang menentukan byte order dan CPU Anda tidak punya suara.

DataView dan Uint32Array memberi angka berbeda dari buffer yang sama. Apakah itu bug?

Bukan, itu perilaku DataView yang terdokumentasi. DataView.getUint32(0) default-nya big-endian, sedangkan Uint32Array selalu mengikuti platform, yaitu little-endian di x86 dan Apple Silicon. Byte yang sama, dua konvensi. Berikan true sebagai argumen ketiga, DataView akan sepakat.

Kenapa ukuran struct saya berubah setelah saya menambahkan <?

Karena Anda beralih dari @, si default, yang memberi padding untuk native alignment. struct.calcsize('@ci') bernilai 8, sedangkan struct.calcsize('=ci') dan struct.calcsize('<ci') sama-sama 5. Tiga byte padding sebelum int ikut pergi bersama native alignment.

Network byte order itu big-endian atau little-endian?

Big-endian. Itu konvensi yang dipakai header TCP/IP, dan karena itu C punya htons dan htonl. Di Python, prefiks ! dan > menghasilkan byte yang identik: struct.pack('!I', 0x12345678) dan struct.pack('>I', 0x12345678) sama-sama memberi 12 34 56 78.

Apakah endianness memengaruhi UTF-8?

Tidak. UTF-8 adalah aliran byte, dan tiap code point ditulis sebagai urutan byte tunggal yang sudah tertata, jadi tidak tersisa satuan multi-byte untuk diurutkan ulang. UTF-16 dan UTF-32 memang punya masalah itu, dan keduanya membawa BOM justru untuk itu, seperti dibahas di bagian 7.

Apakah array satu byte butuh penanganan urutan byte?

Tidak. Urutan byte hanya ada untuk satuan yang lebih lebar dari satu byte, jadi Uint8Array, Int8Array, dan objek bytes di Python semuanya kebal. Naikkan lebarnya satu tingkat, soal itu balik lagi: new Uint16Array(two)[0] = 0x00ff mendarat di memori sebagai ff 00 di mesin ini.

Tag: endianness byte-order binary-data file-formats cross-platform

Artikel Terkait

Lihat semua artikel