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):
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
Kredit: Jeff Dean (research.google.com/people/jeff) dan Peter Norvig (norvig.com)
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