Todas las etiquetas de las entradas, ordenadas
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
N = 100
Tiempo Empate
Memoria pico Empate
N = 10,000
Tiempo Empate
Memoria pico Gana nativo
N = 1,000,000
Tiempo Gana FxDart
Memoria pico Empate
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í.