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:

# 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: