Ruby Numbers and the Memory: cómo me confundí.

Feb 22 2023
De repente estaba haciendo una tarea simple para generar un montón de números enteros aleatorios. No había requisitos estrictos, así que estaba pensando en un límite superior que podría poner.

De repente estaba haciendo una tarea simple para generar un montón de números enteros aleatorios. No había requisitos estrictos, así que estaba pensando en un límite superior que podría poner. Mi instinto me aconsejó encontrar un número que pudiera representar varios números a un costo razonable. Por costo aquí, me refiero a la memoria y el rendimiento.

Logotipo oficial de rubí

Mi primer intento fue 2⁶³-1 como un entero de 8 bytes con signo (1 bit para un signo y 63 bits para el número en sí). Luego, verifiqué .sizepara asegurarme de que solo toma 8 bytes.

(2**63-1).size
=> 8

Por una razón que no recuerdo, decidí buscar cuándo Ruby comenzaría a devolver 9 bytes y, sorprendentemente, para mí, no fueron 2⁶³.

(2**63).size
=> 8

(2**64).size
=> 9

(-1*(2**64-1)).size
=> 8

La respuesta corta: no puede .

Y ahora va la respuesta larga.

En primer lugar, el sizemétodo depende de la máquina .

Pero eso no es todo. Como sabemos, todo en Ruby es un objeto: incluidos los números. Algunos objetos pueden tener un comportamiento diferente a otros debido a la implementación interna. Por ejemplo, Symbolsy Fixnums(ahora con la Bignumparte de la Integerclase) son especiales.

Teniendo esto en cuenta, Integer#sizeno devuelve el tamaño completo del objeto, sino solo su parte numérica.

Para obtener el tamaño real del objeto, debemos usar ObjectSpace.memsize_of en su lugar.

ObjectSpace.memsize_of(2**63)
=> 40

Puede leer más sobre las funciones internas de la memoria de Ruby aquí .

¿Cuál sería el tamaño de 2⁶³-1 en el espacio de objetos de Ruby? Vamos a averiguar.

ObjectSpace.memsize_of(2**63-1)
=> 40

ObjectSpace.memsize_of(2**62-1)
=> 0

Eso significa que Ruby VM no asigna memoria adicional para cada número (de -2⁶² a 2⁶²-1). Almacena el número en su puntero de referencia, que ocupa solo 8 bytes.

¿Por qué es importante saber?

Hay dos razones principales: el rendimiento y el consumo de memoria .

Comencemos con el rendimiento primero.

Operar con Bignum(detrás de escena, tales Integernúmeros aún son Bignum) es más lento.

Podemos comprobar esto con el siguiente punto de referencia simple.


require 'benchmark/ips'

Benchmark.ips do |x|
  x.report('Fixnum') { 2**60 + 2**60 }
  x.report('Bignum') { 2**64 + 2**64 }
end

Warming up --------------------------------------
              Fixnum     1.160M i/100ms
              Bignum   323.084k i/100ms
Calculating -------------------------------------
              Fixnum     11.542M (± 1.0%) i/s -     58.021M in   5.027510s
              Bignum      3.214M (± 0.9%) i/s -     16.154M in   5.026065s

Con alegría o no, los desarrolladores de Ruby rara vez necesitan pensar en problemas de memoria debido al trabajo de Garbage Collector (GC) . Desafortunadamente, a veces esta libertad del control de la memoria puede golpearnos.

Por ejemplo, Ruby rara vez puede liberar la memoria al sistema, lo que significa que preferimos evitar las sobrecargas de memoria . En su mayoría, las sobrecargas de memoria son difíciles de manejar, entonces, ¿por qué no eliminar los problemas potenciales cuando podemos hacerlo sin costo alguno?

En mi caso, crear millones de números aleatorios en unos pocos segundos que tendrían lugar en el espacio de objetos no fue perfecto. Entonces, tener límites entre -2⁶² y 2⁶²-1 para un número aleatorio haría que el consumo de memoria fuera más fluido.

Gracias por su tiempo y síganme para más temas sobre #ruby y #rails.

  • Medio
  • LinkedIn
  • Gorjeo
  • GitHub