Para continuar con el tema de la vez pasada, vamos a ver cómo reducir el tamaño en memoria de los nodos del DOM en librsvg. Desde entonces ha habido cambios en el código; por eso es que en este artículo los nombres de algunos tipos han cambiado con respecto al artículo anterior.
Cada elemento del SVG se representa con esta estructura:
pub struct Element {
element_type: ElementType,
element_name: QualName,
id: Option<String>,
class: Option<String>,
specified_values: SpecifiedValues,
important_styles: HashSet<QualName>,
result: ElementResult,
transform: Transform,
values: ComputedValues,
cond: bool,
style_attr: String,
element_impl: Box<dyn ElementTrait>,
}
Los dos campos más grandes son los que tienen tipos SpecifiedValues
y ComputedValues
. He aquí los tamaños de la estructura completa
Element
y los de esos tipos:
sizeof Element: 1808
sizeof SpecifiedValues: 824
sizeof ComputedValues: 704
En este artículo vamos a reducir el tamaño de SpecifiedValues
.
¿Qué es SpecifiedValues?
Si tenemos un elemento así:
<circle cx="10" cy="10" r="10" stroke-width="4" stroke="blue"/>
Los valores de propiedades de estilos stroke-width
y stroke
se
guardan en un SpecifiedValues
; esta estructura tiene un montón de
campos, uno para cada propiedad de estilos:
pub struct SpecifiedValues {
baseline_shift: SpecifiedValue<BaselineShift>,
clip_path: SpecifiedValue<ClipPath>,
clip_rule: SpecifiedValue<ClipRule>,
/// ...
stroke: SpecifiedValue<Stroke>,
stroke_width: SpecifiedValue<StrokeWidth>,
/// ...
}
Cada campo es un SpecifiedValue<T>
por la siguiente razón. En
CSS/SVG, una propiedad de estilos puede estar no especificada, o ser
inherit
para forzar que se copie la propiedad del elemento padre, o
un valor específico. Librsvg lo representa así:
pub enum SpecifiedValue<T>
where
T: // algunos requerimientos de traits aquí
{
Unspecified,
Inherit,
Specified(T),
}
Ahora bien, SpecifiedValues
tiene un montón de campos, 47 para ser
exactos — uno por cada una de las propiedades de estilos que soporta
librsvg. Por eso es que el tamaño de SpecifiedValues
es de
824 bytes. Es la sub-estructura más grande dentro de Element
, y
sería bueno reducirle el tamaño.
No todas las propiedades se especifican
Volvamos a ver el pedacito de SVG de arriba.
<circle cx="10" cy="10" r="10" stroke-width="4" stroke="blue"/>
Aquí sólo se especifican dos de las propiedades, de modo que los
campos stroke_width
y stroke
de SpecifiedValues
van a quedar
como SpecifiedValue::Specified(algo)
y todos los demás van a quedar
como SpecifiedValue::Unspecified
.
Sería bueno sólo almacenar los valores completos para las propiedades que están especificadas, y una bandera más pequeña para las propiedades que están sin especificar.
Otra forma de representar el conjunto de propiedades
Como hay un máximo de 47 propiedades por elemento (o más si librsvg añade soporte para adicionales), podemos tener un arreglito de 47 bytes. Cada byte contiene el índice en otro arreglo que sólo contiene el valor de una propiedad especificada, o un valor centinela para indicar que la propiedad no está especificada.
Primero hice una enumeración que quepa en un u8
para todas las
propiedades, más el valor centinela al final, que además nos da el
número total de propiedades. El #[repr(u8)]
nos garantiza que ese
enum cabe en un byte.
#[repr(u8)]
enum PropertyId {
BaselineShift,
ClipPath,
ClipRule,
Color,
// ...
WritingMode,
XmlLang,
XmlSpace,
UnsetProperty, // el número de propiedades y el valor centinela
}
Además, desde antes ya había este monstruo para representar "cuál propiedad" además del valor de la propiedad:
pub enum ParsedProperty {
BaselineShift(SpecifiedValue<BaselineShift>),
ClipPath(SpecifiedValue<ClipPath>),
ClipRule(SpecifiedValue<ClipRule>),
Color(SpecifiedValue<Color>),
// ...
}
Cambié la definición de SpecifiedValues
para que tenga dos arreglos,
uno que indica qué propiedades están especificadas, y otro sólo con
las propiedades especificadas:
pub struct SpecifiedValues {
indices: [u8; PropertyId::UnsetProperty as usize],
props: Vec<ParsedProperty>,
}
Hay una cosa que es incómoda en Rust, o que no he sabido resolver
mejor: dada una ParsedProperty
, hay que encontrar el PropertyId
correspondiente para su discriminante. Puse lo obvio:
impl ParsedProperty {
fn get_property_id(&self) -> PropertyId {
use ParsedProperty::*;
match *self {
BaselineShift(_) => PropertyId::BaselineShift,
ClipPath(_) => PropertyId::ClipPath,
ClipRule(_) => PropertyId::ClipRule,
Color(_) => PropertyId::Color,
// ...
}
}
}
Inicialización
Primero, queremos inicializar un SpecifiedValues
vacío, en donde
todo los elementos del arreglo indices
están puesto al valor
sentinela que indica que la propiedad correspondiente no está
especificada:
impl Default for SpecifiedValues {
fn default() -> Self {
SpecifiedValues {
indices: [PropertyId::UnsetProperty.as_u8(); PropertyId::UnsetProperty as usize],
props: Vec::new(),
}
}
}
Eso pone el campo indices
a un arreglo lleno del valor sentinela
PropertyId::UnsetProperty
. Además, el arreglo props
está vacío; ni
siquiera se ha pedido un bloque de memoria para él. Así, los
elementos del SVG que no tienen propiedades de estilos no ocupan
memoria extra.
¿Qué propiedades están especificadas y cuáles son sus índices?
Segundo, queremos una función que nos dé el índice en props
de
alguna propiedad, o que nos diga si esa propiedad no está especificada
aún:
impl SpecifiedValues {
fn property_index(&self, id: PropertyId) -> Option<usize> {
let v = self.indices[id.as_usize()];
if v == PropertyId::UnsetProperty.as_u8() {
None
} else {
Some(v as usize)
}
}
}
(Si alguien pasa id = PropertyId::UnsetProperty
, el acceso al
arreglo indices
va a mandar un panic, que es lo que queremos, pues
ese no es identificador válido para una propiedad.)
Cambiar el valor de una propiedad
Tercero, queremos poner el valor de una propiedad que no estaba especificada, o cambiar el valor de una que ya lo estaba:
impl SpecifiedValues {
fn replace_property(&mut self, prop: &ParsedProperty) {
let id = prop.get_property_id();
if let Some(index) = self.property_index(id) {
self.props[index] = prop.clone();
} else {
self.props.push(prop.clone());
let pos = self.props.len() - 1;
self.indices[id.as_usize()] = pos as u8;
}
}
}
En el primer caso del if
, la propiedad ya estaba puesta y nada más
remplazamos su valor. En el segundo caso, la propiedad no estaba
puesta; la añadimos al arreglo props
y guardamos su índice
resultante en indices
.
Resultados
Antes:
sizeof Element: 1808
sizeof SpecifiedValues: 824
Después:
sizeof Element: 1056
sizeof SpecifiedValues: 72
El archivo patológico de la vez anterior consumía 463,412,720 bytes en memoria antes de estos cambios. Después de los cambios, consume 314,526,136 bytes.
También medí el consumo de memoria de un un archivo normal, en este caso uno con muchos de los iconos simbólicos de GNOME. La versión anterior consume 17 MB; la versión nueva sólo 13 MB.
Cómo seguir ajustando esto
Por ahora, estoy satisfecho con SpecifiedValues
, aunque todavía se
podría hacer más pequeño:
-
El crate tagged-box convierte un enum como el
ParsedProperty
en un enum-de-boxes, y codifica el discriminante del enum en el puntero que apunta al box. De esta forma cada variante ocupa el mínimo posible de memoria, aunque se le suma un bloque de memora extra, y el contenedor en sí ocupa un solo puntero. No estoy seguro si valga la pena; cadaParsedProperty
ocupa 64 bytes, pero el arreglo plano deprops: Vec<ParsedProperty>
queda muy lindo en un solo bloque de memoria. No he visto los tamaños de cada propiedad individual para ver si varían mucho entre sí. -
Buscar un crate para poder tener las propiedades en un sólo bloque de memoria, una especie de arena de tipos variables. Esto se puede implementar con un poquito de
unsafe
, pero hay que tener cuidado con la alineación de los elementos de diferentes tipos. -
El crate enum_set2 representa un arreglo de enums sin campos como un arreglo de bits compacto. Si se cambiara la representación de
SpecifiedValue
, esto reduciría el arregloindices
al mínimo.
Si alguien quiere dedicarle tiempo a implementar y medir algo así, le estaría muy agradecido.
Siguientes pasos
Según Massif, lo siguiente es seguir haciendo que Element
sea más
pequeño. Lo que sigue de reducir de tamaño es ComputedValues
. La
opción obvia es hacerle exactamente lo mismo que a SpecifiedValues
.
No estoy seguro de si vale más la pena intentar compartir las
estructuras de estilos entre varios elementos.