Todas las etiquetas de las entradas, ordenadas

Empate

Requisito

Cada entrada del blog lleva una lista de etiquetas. Construye el índice de etiquetas del sitio: aplana las etiquetas de todas las entradas en una sola secuencia, elimina los duplicados, ordénalas alfabéticamente e imprímelas en una única línea separada por comas. Los datos están en el código de abajo; ambas versiones deben imprimir la línea que aparece bajo Salida esperada.

Salida esperada
async, concurrency, dart, fp, iterables, recipes, streams

Lado a lado

Dart nativo

FxDart

Por qué difieren

Sobre el papel, apenas difieren. expand es el flatMap de Dart, toSet() elimina duplicados y una cascada ..sort() remata el trabajo — esa cadena es Dart honesto e idiomático, y no tiene nada de malo. FxDart deletrea esos mismos tres pasos como eslabones con nombre de una cadena (flatMap → uniq → sort), lo que se lee un poco más como el requisito y deja explícito que uniq preserva el orden, en lugar de que sea un efecto secundario de haber elegido un Set. Como código, esto es un empate.

El reloj no empata. Las barras de Benchmark de abajo ponen a FxDart a 1,47× la velocidad de la cadena nativa con un millón de entradas — 73,5 ms frente a 108,0 ms — y la proporción se mantiene hasta abajo (1,36× con N=10.000, 1,29× con N=100; esas dos escalas siguen llevando la insignia misma velocidad sólo porque la diferencia absoluta ahí queda por debajo del umbral de percepción de 0,6 ms que usa el sitio). Ambos lados hacen exactamente el mismo trabajo: tres millones de cadenas de etiqueta tiradas a través de un aplanador, metidas en un conjunto hash que retiene 500 valores distintos, y una ordenación de 500 elementos. El algoritmo no cambia en nada.

Toda la diferencia vive en un campo del ExpandIterator de dart:core. Inicializa su ranura de iterador interno con un centinela const EmptyIterator<Never>() para poder aplazar la primera llamada al callback, y eso hace que la línea caliente — _currentExpansion!.moveNext(), ejecutada una vez por cada etiqueta emitida — vea dos clases receptoras a lo largo del bucle. Con eso basta para que AOT no pueda incorporar (inline) el iterador interno de List, así que los tres millones de avances internos se convierten en llamadas indirectas. El flatMap de FxDart guarda un simple Iterator<B>? que sólo contiene el iterador interno real y usa null para «todavía no hay ninguno abierto», de modo que ese mismo punto de llamada se mantiene monomórfico y se incorpora.

No es una conjetura. Cambiando sólo el aplanador — el flatMap de FxDart alimentando el propio toSet().toList()..sort() nativo — ya se baja a 78 ms; cambiando sólo el otro extremo, el expand del núcleo hacia uniq y sort, se queda en 108 ms. Copiando ExpandIterator a mano dentro del benchmark y tocando nada más que ese centinela (iterador vacío → null) se pasa de 105 ms a 77 ms por sí solo; la otra diferencia de forma, el _current anulable del núcleo leído mediante un cast, no cuesta nada medible. Cada variante se compiló con AOT como binario propio, porque meterlas todas en un mismo programa vuelve polimórfico cada punto de llamada a moveNext y borra justo el efecto que se quiere medir.

Dos matices que conviene retener. Esto es un detalle de implementación del SDK, no una ley: el día que ExpandIterator se deshaga de ese centinela, expand igualará a FxDart y esta página volverá a ser un empate en ambas columnas. Y un bucle for anidado escrito a mano que añade directamente a un Set gana a los dos, con 43 ms — abandonar del todo el protocolo de iteradores sigue siendo lo más rápido que se puede hacer aquí. Lo que la medición descarta es la suposición de que recurrir a un pipeline con nombre te cuesta velocidad frente a la cadena idiomática del núcleo. Aquí te la da.

Benchmark

Apple M1 Max, 32 GB de RAM · Dart 3.12.2 (compilado AOT) · 2026-08-24

N = 100

Tiempo Empate

Dart nativo 60 µs
FxDart 48 µs

Memoria pico Empate

Dart nativo 15.9 MB
FxDart 16.5 MB

N = 10,000

Tiempo Empate

Dart nativo 1.25 ms
FxDart 920 µs

Memoria pico Gana nativo

Dart nativo 20.6 MB
FxDart 23.2 MB

N = 1,000,000

Tiempo Gana FxDart

Dart nativo 106.9 ms
FxDart 77.8 ms

Memoria pico Empate

Dart nativo 161.7 MB
FxDart 161.8 MB

Las barras son medianas de iteraciones cronometradas repetidas en procesos nuevos por lado (los N pequeños se agrupan por resolución del temporizador). Dos lados a menos del 5% entre sí — o a menos de 0.6 ms, una diferencia que nadie puede percibir — cuentan como empate; las carreras relativas ajustadas se vuelven a medir hasta 5 veces. En una app, cualquier cosa por debajo de unos pocos milisegundos es invisible para el usuario, gane la barra que gane. La memoria es el RSS pico del proceso. La VM de Dart y el dataset son idénticos en ambos lados, así que la diferencia entre las dos barras es lo que retiene el pipeline en sí.