Latency Numbers Every Programmer Should Know

Nomor Latensi yang Harus Diketahui Setiap Programmer

Dalam dunia pemrograman, memahami waktu latensi berbagai operasi komputer adalah kunci untuk mengembangkan aplikasi yang efisien dan responsif. Latensi adalah waktu yang dibutuhkan untuk menyelesaikan suatu operasi, seperti mengakses memori, membaca data dari disk, atau mengirim data melalui jaringan. Angka-angka latensi ini, yang awalnya dipopulerkan oleh Jeff Dean dan Peter Norvig, memberikan panduan tentang seberapa cepat atau lambat operasi tertentu berjalan pada perangkat keras modern. Artikel ini menyajikan daftar nomor latensi, contoh kode dalam bahasa Go (Golang), dan penjelasan mendalam untuk membantu programmer memahami dan mengoptimalkan kode mereka.

Daftar Nomor Latensi

Berikut adalah daftar nomor latensi yang harus diketahui setiap programmer, berdasarkan sumber terpercaya (GitHub Gist oleh Jonas Bonér):

Operasi
Waktu dalam ns
Waktu dalam µs
Waktu dalam ms
Catatan

Referensi cache L1

0.5 ns

Mispredict cabang

5 ns

Referensi cache L2

7 ns

14x cache L1

Kunci/buka mutex

25 ns

Referensi memori utama

100 ns

20x cache L2, 200x cache L1

Kompresi 1K byte dengan Zippy

3.000 ns

3 µs

Kirim 1K byte melalui jaringan 1 Gbps

10.000 ns

10 µs

Baca 4K secara acak dari SSD*

150.000 ns

150 µs

~1GB/detik SSD

Baca 1 MB secara berurutan dari memori

250.000 ns

250 µs

Perjalanan pulang-pergi dalam datacenter yang sama

500.000 ns

500 µs

Baca 1 MB secara berurutan dari SSD*

1.000.000 ns

1.000 µs

1 ms

~1GB/detik SSD, 4X memori

Disk seek

10.000.000 ns

10.000 µs

10 ms

20x perjalanan pulang-pergi datacenter

Baca 1 MB secara berurutan dari disk

20.000.000 ns

20.000 µs

20 ms

80x memori, 20X SSD

Kirim paket CA->Belanda->CA

150.000.000 ns

150.000 µs

150 ms

Catatan:

  • 1 ns = 10^-9 detik

  • 1 µs = 10^-6 detik = 1.000 ns

  • 1 ms = 10^-3 detik = 1.000 µs = 1.000.000 ns

  • Angka-angka ini adalah perkiraan dan dapat bervariasi tergantung pada perangkat keras dan konfigurasi sistem.

  • Sumber: GitHub Gist oleh Jonas Bonér, berdasarkan karya Jeff Dean dan Peter Norvig (norvig.com).

Penjelasan dan Contoh Kode

Berikut adalah penjelasan mendalam untuk setiap operasi, beserta contoh kode dalam Go untuk mengilustrasikan operasi tersebut. Perhatikan bahwa waktu yang diukur dalam contoh kode mungkin tidak mencerminkan latensi yang tepat karena faktor seperti overhead sistem operasi, tetapi kode ini dirancang untuk memberikan gambaran tentang operasi tersebut.

1. Referensi Cache L1: 0.5 ns

Penjelasan: Referensi cache L1 adalah operasi tercepat dalam hirarki memori. Cache L1 adalah memori kecil dan sangat cepat yang terletak di dalam atau sangat dekat dengan prosesor. Data yang sering diakses disimpan di sini untuk mempercepat pemrosesan. Latensi 0.5 ns menunjukkan betapa cepatnya akses ini dibandingkan operasi lain.

Contoh Kode Go:

package main

import (
	"fmt"
	"time"
)

func main() {
	// Inisialisasi array kecil (kemungkinan besar muat di cache L1)
	arr := make([]int, 1000)
	for i := range arr {
		arr[i] = i
	}

	start := time.Now()
	// Akses elemen array
	sum := 0
	for _, v := range arr {
		sum += v
	}
	end := time.Now()

	fmt.Printf("Waktu eksekusi: %v\n", end.Sub(start))
	fmt.Printf("Jumlah: %d\n", sum)
}

Penjelasan Contoh: Array kecil dengan 1.000 elemen kemungkinan besar akan muat di cache L1 (biasanya berukuran beberapa KB). Operasi penjumlahan dalam loop ini akan memanfaatkan cache L1 untuk akses data yang cepat. Waktu eksekusi yang diukur memberikan indikasi kasar tentang performa akses cache L1.

2. Mispredict Cabang: 5 ns

Penjelasan: Mispredict cabang terjadi ketika prosesor salah menebak arah cabang kondisional (misalnya, if-else) dalam kode. Prosesor modern menggunakan prediksi cabang untuk mempercepat eksekusi, tetapi jika prediksi salah, prosesor harus membuang instruksi yang sudah diproses, menyebabkan penundaan sekitar 5 ns.

Contoh Kode Go:

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	count := 0
	for i := 0; i < 100000000; i++ {
		if (i % 2) == 0 {
			count++
		}
	}
	end := time.Now()

	fmt.Printf("Waktu eksekusi: %v\n", end.Sub(start))
	fmt.Printf("Count: %d\n", count)
}

Penjelasan Contoh: Kondisi if (i % 2) == 0 menghasilkan pola true-false yang bergantian, yang dapat menyebabkan mispredict cabang karena sulit diprediksi oleh prosesor. Loop besar digunakan untuk membuat efek latensi lebih terlihat.

3. Referensi Cache L2: 7 ns (14x Cache L1)

Penjelasan: Cache L2 lebih besar dari cache L1 tetapi lebih lambat, dengan latensi sekitar 7 ns. Cache ini digunakan ketika data tidak ditemukan di cache L1, tetapi masih lebih cepat daripada mengakses memori utama.

Contoh Kode Go:

package main

import (
	"fmt"
	"time"
)

func main() {
	// Inisialisasi array besar (kemungkinan tidak sepenuhnya muat di cache L1)
	arr := make([]int, 1000000)
	for i := range arr {
		arr[i] = i
	}

	start := time.Now()
	// Akses elemen array
	sum := 0
	for _, v := range arr {
		sum += v
	}
	end := time.Now()

	fmt.Printf("Waktu eksekusi: %v\n", end.Sub(start))
	fmt.Printf("Jumlah: %d\n", sum)
}

Penjelasan Contoh: Array dengan 1.000.000 elemen (sekitar 4 MB untuk integer 32-bit) kemungkinan besar tidak muat sepenuhnya di cache L1, sehingga akses data akan melibatkan cache L2.

4. Kunci/Buka Mutex: 25 ns

Penjelasan: Mutex (mutual exclusion) digunakan untuk mengelola akses bersamaan ke sumber daya dalam program konkuren. Operasi mengunci dan membuka mutex membutuhkan waktu sekitar 25 ns karena melibatkan koordinasi tingkat sistem operasi.

Contoh Kode Go:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var mu sync.Mutex
	start := time.Now()
	for i := 0; i < 10000000; i++ {
		mu.Lock()
		mu.Unlock()
	}
	end := time.Now()

	fmt.Printf("Waktu eksekusi: %v\n", end.Sub(start))
}

Penjelasan Contoh: Kode ini mengunci dan membuka mutex sebanyak 10.000.000 kali dalam loop. Waktu eksekusi mencerminkan latensi operasi mutex, meskipun overhead lain mungkin memengaruhi pengukuran.

5. Referensi Memori Utama: 100 ns (20x Cache L2, 200x Cache L1)

Penjelasan: Jika data tidak ditemukan di cache L1 atau L2, prosesor harus mengakses memori utama (RAM), yang memiliki latensi sekitar 100 ns. Ini jauh lebih lambat dibandingkan cache.

Contoh Kode Go:

package main

import (
	"fmt"
	"time"
)

func main() {
	// Inisialisasi array besar (pasti tidak muat di cache)
	arr := make([]int, 100000000)
	for i := range arr {
		arr[i] = i
	}

	start := time.Now()
	// Akses elemen array
	sum := 0
	for _, v := range arr {
		sum += v
	}
	end := time.Now()

	fmt.Printf("Waktu eksekusi: %v\n", end.Sub(start))
	fmt.Printf("Jumlah: %d\n", sum)
}

Penjelasan Contoh: Array dengan 100.000.000 elemen (sekitar 400 MB) pasti tidak muat di cache, sehingga akses data akan langsung ke memori utama.

6. Kompresi 1K Byte dengan Zippy: 3.000 ns (3 µs)

Penjelasan: Zippy (sekarang dikenal sebagai Snappy) adalah algoritma kompresi cepat yang dikembangkan oleh Google. Mengompresi 1 KB data membutuhkan waktu sekitar 3 µs.

Contoh Kode Go: Go tidak memiliki pustaka Zippy/Snappy bawaan, tetapi kita bisa menggunakan compress/flate sebagai alternatif untuk mengilustrasikan kompresi.

package main

import (
	"bytes"
	"compress/flate"
	"fmt"
	"time"
)

func main() {
	data := make([]byte, 1024)
	for i := range data {
		data[i] = byte(i)
	}

	var buf bytes.Buffer
	w, _ := flate.NewWriter(&buf, flate.BestSpeed)
	start := time.Now()
	w.Write(data)
	w.Close()
	end := time.Now()

	fmt.Printf("Waktu kompresi: %v\n", end.Sub(start))
}

Penjelasan Contoh: Kode ini mengompresi 1 KB data menggunakan algoritma flate dengan pengaturan kecepatan terbaik. Waktu yang diukur memberikan gambaran tentang latensi kompresi.

7. Kirim 1K Byte melalui Jaringan 1 Gbps: 10.000 ns (10 µs)

Penjelasan: Mengirim 1 KB data melalui jaringan dengan kecepatan 1 Gbps membutuhkan waktu sekitar 10 µs, termasuk overhead protokol jaringan.

Contoh Kode Go:

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	// Server
	go func() {
		ln, _ := net.Listen("tcp", ":8080")
		conn, _ := ln.Accept()
		buf := make([]byte, 1024)
		conn.Read(buf)
		conn.Close()
	}()

	// Client
	conn, _ := net.Dial("tcp", "localhost:8080")
	data := make([]byte, 1024)
	start := time.Now()
	conn.Write(data)
	conn.Close()
	end := time.Now()

	fmt.Printf("Waktu pengiriman: %v\n", end.Sub(start))
}

Penjelasan Contoh: Kode ini mensimulasikan pengiriman 1 KB data dari klien ke server lokal melalui koneksi TCP. Waktu yang diukur mencakup latensi pengiriman data.

8. Baca 4K Secara Acak dari SSD: 150.000 ns (150 µs)

Penjelasan: Membaca 4 KB data secara acak dari SSD membutuhkan waktu sekitar 150 µs karena SSD harus mencari lokasi data yang tidak berurutan.

Contoh Kode Go:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	file, _ := os.Open("largefile.dat") // Asumsi file besar ada di SSD
	buf := make([]byte, 4096)
	start := time.Now()
	file.ReadAt(buf, 1000000) // Baca secara acak
	end := time.Now()

	fmt.Printf("Waktu pembacaan: %v\n", end.Sub(start))
	file.Close()
}

Penjelasan Contoh: Kode ini membaca 4 KB data dari posisi acak (offset 1.000.000) dalam file besar yang diasumsikan berada di SSD. Operasi ini mensimulasikan akses acak.

9. Baca 1 MB Secara Berurutan dari Memori: 250.000 ns (250 µs)

Penjelasan: Membaca 1 MB data secara berurutan dari memori utama membutuhkan waktu sekitar 250 µs. Akses berurutan lebih cepat daripada akses acak karena memanfaatkan prefetching memori.

Contoh Kode Go: Lihat contoh pada Referensi Memori Utama (poin 5), karena operasi ini serupa tetapi dengan ukuran data yang lebih besar.

10. Perjalanan Pulang-Pergi dalam Datacenter yang Sama: 500.000 ns (500 µs)

Penjelasan: Waktu untuk mengirim dan menerima data dalam datacenter yang sama adalah sekitar 500 µs, mencerminkan latensi jaringan internal.

Contoh Kode Go:

package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	start := time.Now()
	resp, _ := http.Get("http://localhost:8080") // Asumsi server lokal
	resp.Body.Close()
	end := time.Now()

	fmt.Printf("Waktu perjalanan pulang-pergi: %v\n", end.Sub(start))
}

Penjelasan Contoh: Kode ini mengirim permintaan HTTP GET ke server lokal dan mengukur waktu perjalanan pulang-pergi. Anda perlu menjalankan server lokal (misalnya, dengan http.Server) untuk menguji ini.

11. Baca 1 MB Secara Berurutan dari SSD: 1.000.000 ns (1 ms)

Penjelasan: Membaca 1 MB data secara berurutan dari SSD membutuhkan waktu sekitar 1 ms, jauh lebih cepat daripada akses acak karena SSD dioptimalkan untuk pembacaan berurutan.

Contoh Kode Go:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	file, _ := os.Open("largefile.dat") // Asumsi file besar ada di SSD
	buf := make([]byte, 1024*1024) // 1 MB
	start := time.Now()
	file.Read(buf)
	end := time.Now()

	fmt.Printf("Waktu pembacaan: %v\n", end.Sub(start))
	file.Close()
}

Penjelasan Contoh: Kode ini membaca 1 MB data secara berurutan dari file besar yang diasumsikan berada di SSD.

12. Disk Seek: 10.000.000 ns (10 ms)

Penjelasan: Disk seek adalah waktu yang dibutuhkan untuk memindahkan kepala baca/tulis pada hard disk drive (HDD) ke posisi tertentu, sekitar 10 ms. Ini jauh lebih lambat dibandingkan operasi SSD.

Contoh Kode Go:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	file, _ := os.Open("largefile.dat") // Asumsi file besar ada di HDD
	buf := make([]byte, 4096)
	start := time.Now()
	file.ReadAt(buf, 0)       // Baca dari awal
	file.ReadAt(buf, 1000000) // Baca dari posisi lain, menyebabkan seek
	end := time.Now()

	fmt.Printf("Waktu dengan seek: %v\n", end.Sub(start))
	file.Close()
}

Penjelasan Contoh: Kode ini membaca data dari dua posisi berbeda dalam file besar yang diasumsikan berada di HDD, menyebabkan operasi disk seek.

13. Baca 1 MB Secara Berurutan dari Disk: 20.000.000 ns (20 ms)

Penjelasan: Membaca 1 MB data secara berurutan dari HDD membutuhkan waktu sekitar 20 ms, jauh lebih lambat dibandingkan SSD karena keterbatasan mekanis HDD.

Contoh Kode Go: Lihat contoh pada Baca 1 MB Secara Berurutan dari SSD (poin 11), tetapi asumsikan file berada di HDD.

14. Kirim Paket CA->Belanda->CA: 150.000.000 ns (150 ms)

Penjelasan: Mengirim paket data dari California ke Belanda dan kembali membutuhkan waktu sekitar 150 ms, mencerminkan latensi jaringan internasional yang signifikan.

Contoh Kode Go:

package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	start := time.Now()
	resp, _ := http.Get("http://example.com") // Asumsi server di Belanda
	resp.Body.Close()
	end := time.Now()

	fmt.Printf("Waktu perjalanan pulang-pergi: %v\n", end.Sub(start))
}

Penjelasan Contoh: Kode ini mengirim permintaan HTTP GET ke server yang diasumsikan berada di Belanda dan mengukur waktu perjalanan pulang-pergi.

Mengapa Nomor Latensi Ini Penting?

Memahami nomor latensi membantu programmer dalam beberapa cara:

  • Optimasi Performa: Dengan mengetahui bahwa akses memori utama 200 kali lebih lambat daripada cache L1, programmer dapat meminimalkan akses memori utama dengan menyimpan data di cache.

  • Desain Sistem: Dalam sistem terdistribusi, latensi jaringan seperti perjalanan pulang-pergi antar datacenter (500 µs) atau internasional (150 ms) dapat memengaruhi desain arsitektur.

  • Menghindari Bottleneck: Operasi seperti disk seek (10 ms) atau baca dari HDD (20 ms) jauh lebih lambat, sehingga programmer dapat memilih SSD atau memori untuk performa lebih baik.

Keterbatasan dan Catatan

  • Variasi Perangkat Keras: Angka-angka ini adalah perkiraan dan dapat bervariasi tergantung pada perangkat keras. Misalnya, SSD modern mungkin lebih cepat dari 1 ms untuk membaca 1 MB.

  • Overhead Sistem: Contoh kode di atas mungkin tidak mencerminkan latensi yang tepat karena overhead dari sistem operasi, runtime Go, atau faktor lain.

  • Konteks Penggunaan: Latensi ini adalah "napkin estimations" (perkiraan kasar) dan tidak memperhitungkan faktor seperti antrian atau konkurensi.

Sumber

Kesimpulan

Daftar nomor latensi ini adalah alat penting bagi programmer untuk memahami performa operasi komputer. Dengan contoh kode Go dan penjelasan di atas, Anda dapat melihat bagaimana operasi ini diterapkan dalam praktik dan bagaimana latensi memengaruhi desain program. Dengan memanfaatkan pengetahuan ini, Anda dapat menulis kode yang lebih efisien, menghindari bottleneck, dan menciptakan aplikasi yang lebih responsif.

Last updated