Binarios Eficientes
Técnicas de bajo nivel para manipulación eficiente de binarios. ⏱️ 2.5 horas
Anatomía de binarios en la BEAM
La BEAM tiene varios tipos de binarios internos:
- Heap binaries: ≤64 bytes, viven en el heap del proceso
- Refc binaries: >64 bytes, referencia compartida en heap global
- Sub-binaries: puntero a porción de otro binario (zero-copy)
# Sub-binary: no copia datos
data = <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10>>
<<_::binary-size(2), rest::binary>> = data
# 'rest' apunta a data[2:], sin copiar
# Forzar copia cuando el original es muy grande
small_copy = :binary.copy(rest)
IOLists: evitar copias
# MAL: concatenación crea copias
result = header <> body <> footer
# BIEN: IOList, se concatena al escribir a socket
result = [header, body, footer]
:gen_tcp.send(socket, result)
# IOData puede mezclar binarios, bytes y listas
iodata = [1, 2, "hello", [3, <<4, 5>>]]
IO.iodata_length(iodata) # => 10
IO.iodata_to_binary(iodata) # Solo al final si es necesario
Binary comprehensions
# Crear binario eficientemente
binary = for x <- 1..100, into: <<>> do
<<x::8>>
end
# Parsear binario
for <<byte <- data>> do
byte
end
# Con tamaños específicos
for <<value::32 <- data>> do
value
end
Optimizaciones críticas
Evitar append en loops
# MAL: O(n²) complejidad
def build_slow(items) do
Enum.reduce(items, <<>>, fn item, acc ->
acc <> encode(item) # Copia acc cada vez!
end)
end
# BIEN: O(n) con IOList
def build_fast(items) do
items
|> Enum.map(&encode/1)
|> IO.iodata_to_binary()
end
Preallocación con :binary.copy
# Para construir binario grande incrementalmente
def build_preallocated(size) do
# Reservar espacio
buffer = :binary.copy(<<0>>, size)
# Escribir en posiciones específicas
# (requiere NIF para ser eficiente)
end
Reference counting y GC
# Binarios grandes son reference-counted
# Si mantienes una referencia pequeña a uno grande,
# el grande no se libera
large = :crypto.strong_rand_bytes(10_000_000)
<<small::binary-size(10), _::binary>> = large
# 'small' es sub-binary, 'large' sigue en memoria!
# Solución: copiar el fragmento que necesitas
small = :binary.copy(small)
Benchmarking binarios
# Medir con :timer.tc
{time_us, result} = :timer.tc(fn ->
build_message(data)
end)
IO.puts("Took #{time_us} microseconds")
# Medir memoria con :erlang.memory
before = :erlang.memory(:binary)
result = operation()
after = :erlang.memory(:binary)
IO.puts("Binary memory: #{after - before} bytes")
Ejercicio 17.1
Optimizar encoder
Intermedio
Toma el encoder de MarketProtocol y optimízalo:
- Usar IOLists en lugar de concatenación
- Precalcular headers constantes
- Medir mejora con Benchee
Ejercicio 17.2
Memory profiling
Avanzado
Analiza el uso de memoria de tu parser:
- Identificar binarios que no se liberan
- Forzar copias donde sea necesario
- Medir antes/después de optimizaciones
Conexión con el proyecto final
Para máximo rendimiento en el sistema de distribución:
- IOLists para broadcast: construir mensaje una vez, enviar a muchos
- Sub-binarios para parsing: zero-copy al decodificar
- Liberación explícita: copiar fragmentos pequeños de mensajes grandes