Escribir pipelines rápidos

La biblioteca puede hacer que una forma sea rápida; lo que no puede es elegir la forma por ti. Estas son las que compensan.

// Shapes that let a terminal operator take a fast path fx(list).map(f).toList() // hands the copy to the SDK's bulk fill fx(list).filter(p).toList() // same fx(list).map(f).distinct() // fuses into one stage, one loop foldBy(key, seed, f, list) // no per-key List, unlike groupBy + fold countBy(key, list) // foldBy with a counter, pre-named uniqStrict(list) // dedupe once when the result is reused

Lección

Cada ejemplo de la comparativa Dart vs FxDart se mide contra un bucle imperativo escrito a mano, y la mayoría se queda a su altura o muy cerca. Cuando una cadena es más lenta, casi nunca es el algoritmo — ambos lados hacen el mismo trabajo — y casi siempre es una de cuatro cosas: una frontera entre etapas pagada por elemento, una asignación por elemento, un callback que se ejecuta más veces de las necesarias, o un origen que se recorre más de una vez.

1. Termina en un operador terminal

Un terminal como toList ve la cadena entera y puede tomar una ruta que ningún consumidor elemento a elemento puede. Sobre un origen List, map(f).toList() y filter(p).toList() delegan la copia al relleno masivo del propio SDK, que escribe el resultado sin la comprobación de tipo por elemento que el código de paquete está obligado a pagar. Tirar de la misma cadena a mano con un for-in e ir acumulando renuncia por completo a esa ruta.

2. Filtra antes de mapear

Las etapas se ejecutan en el orden en que las escribes, y cada elemento que sobrevive a un filter paga todas las etapas posteriores. Poner la prueba barata primero y la transformación cara después es gratis y a menudo la mayor mejora disponible.

3. Pide la respuesta, no los ingredientes

groupBy construye una List por cada clave — asignación proporcional a la entrada — y si lo único que querías era un total por clave, esas listas se construyen y se tiran. foldBy acumula directamente en el mapa de resultado, y countBy es la versión con contador, ya con nombre. Recurre a groupBy cuando de verdad quieras los miembros.

Estos dos son además el caso raro en que el operador es más rápido que el bucle que habrías escrito. La línea obvia, counts[k] = (counts[k] ?? 0) + 1, toca la tabla hash dos veces por elemento — una para leer y otra para volver a escribir — y en un trabajo de conteo la tabla es prácticamente todo el coste. Ambos operadores cuentan en una celda mutable alojada en la tabla, así que esta se escribe una vez por clave distinta en lugar de una vez por elemento: ~1,5× al contar un millón de filas, y es la razón por la que Nivel de log más frecuente le gana a un bucle a mano en vez de quedarse por detrás.

4. Una cadena perezosa se reejecuta en cada pasada

La pereza significa que la cadena es una receta, no un resultado: recórrela dos veces y el origen se recorre dos veces. Cuando la respuesta se usa más de una vez, materialízala una sola vez — con toList, o con uniqStrict cuando la propia deduplicación es lo que conservas.

Lo que la pereza te sigue dando

Nada de esto es un argumento contra las cadenas perezosas. Un take o un head después de un filtro detienen el origen en cuanto tienen suficiente — el trabajo sencillamente no llega a ocurrir — y ese es un tipo de ahorro que ninguna pipeline ansiosa puede igualar. La pereza cuesta un poco por elemento y puede ahorrarlo todo.

Los records no son gratis. Una etapa que produce un record — zip, zipWithIndex, pairwise, o un map a una tupla — asigna uno por elemento, y un record no se puede reutilizar. Si la etapa siguiente descarta la mayoría, plantéate si la pipeline puede llevar índices o un solo valor; mapWithIndex existe precisamente para que zipWithIndex().map(...) no tenga que asignar un par por elemento.

Demo 1 · Terminales y orden del filter

Demo 2 · foldBy en lugar de groupBy, y el precio de una segunda pasada

Pruébalo tú

Ejercicio: el fragmento recorre sus lecturas dos veces y construye una lista que luego tira. Reescríbelo como una única cadena que termine en un solo operador terminal.

Relacionados: toList — el terminal del que cuelgan casi todas las rutas rápidas · foldBy — agregar sin los grupos · uniqStrict — deduplicar una vez, reutilizar muchas · mapWithIndex — el índice sin el record