Diario
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Progreso
Sección titulada «Progreso»- Código del curso: el programa de las lecciones, los ejemplos descompilados, los fragmentos rechazados y los benchmarks, comparados con su salida esperada por
check.sh - CI en Linux, Windows y macOS, con una ejecución de prueba (dry run) de cada benchmark
- Lección 1: memoria, valores, referencias y spans
- Lección 2: el recolector de basura
- Lección 3: async y await por dentro
- Lección 4: rendimiento medido
- Lección 5: genéricos en profundidad
- Un nuevo plan en cuatro partes y 24 lecciones, con ASP.NET Core en profundidad y los equivalentes en Spring y Reactor
- Lección 6: canales
- Lección 7: TPL Dataflow
- Lección 8: Rx.NET
- Lección 9: elegir un flujo
- Lecciones 10-14: estado compartido y ruta de una petición ASP.NET Core
- Lección 15: servicios hospedados y trabajo en segundo plano
- Lección 16: autenticación y autorización
- Lección 17: traducción de un oráculo de especificación Petri acotado en diseño de pruebas deterministas para Channel, TPL Dataflow y Rx
- Apéndice 1: cinco miembros de GA optimizados, demostrados sobre los 4096 conjuntos de clases de altura y luego medidos — con los benchmarks reescritos en cuanto se vio que medían el JIT
- Apéndice 2: el pipeline de indexación de GA perfilado, tres cambios demostrados contra la propia salida de GA y medidos, y enviados aguas arriba como pull requests
- Apéndice 4: plugins sin MEF — cuatro módulos, cada uno en su propio contexto descargable, un único contenedor, siete escenarios predichos y luego medidos
Este curso perfila GuitarAlchemist/ga, un repositorio público, y corre sobre .NET 10 y BenchmarkDotNet: todo lo que sigue es código de otros, incluso donde el autor de GA y el del curso son la misma persona. Tres filas subieron aguas arriba como pull requests y se fusionaron; las demás nunca se reportaron, y la columna Estado dice cuál es cuál.
| Esperado | Lo que pasa | Dónde | Medición | Estado |
|---|---|---|---|---|
| Reconocer un acorde para cada voicing cuesta lo que cuesta un reconocimiento | 667 125 voicings usan solo unos 2500 conjuntos de clases de altura distintos, así que la búsqueda de patrón de cada conjunto se repetía unas 266 veces | CanonicalChordRecognizer.IdentifyChordSet |
dotnet-trace situaba allí cerca del 52 % del hilo principal, en su mayoría construyendo HashSet. Con una caché de 4096 casillas, VoicingAnalyzer.Analyze pasó de 234,34 ms y 759 MB a 8,94 ms y 22 MB |
Corregido aguas arriba, PR #695 2026-09-17 |
| El vector de clases de intervalo se calcula una vez por conjunto | Se recalculaba en cada llamada | la teoría atonal de GA | El mismo camino de análisis; el volcado de los 8192 vectores es idéntico byte a byte antes y después | Corregido aguas arriba, PR #694 2026-09-17 |
OptickIndexReader.Dimension se lee una vez |
Se leía en cada consulta, mediante una propiedad que lanza un Where + Sum de LINQ sobre el registro de particiones |
OptickIndexReader.Dimension |
Los 38 MB por consulta atribuidos a la búsqueda se asignaban ahí, no en Parallel.For: un barrido secuencial asignaba los mismos 38 MB |
Corregido aguas arriba, PR #693 2026-09-17 |
| La exportación del índice OPTIC-K es reproducible desde un commit | La primera exportación de la noche difiere de las siguientes en 266 de 313 047 entradas, con el mismo commit de GA | exportación OPTIC-K | 266 entradas de 313 047, identificadas por instrumento y diagrama | Corregido upstream por #730, fusionada el 2026-09-24. La causa era el generador paralelo de voicings, cuyo orden de salida seguía la planificación de los hilos; dos exportaciones de guitarra del main de GA tenían hashes distintos antes, idénticos después 2026-09-17, 2026-09-24 |
El camino paralelo de VoicingGenerator es más rápido que el secuencial |
Era el doble de lento: un solo consumidor construía todos los diagramas y descartaba los duplicados | VoicingGenerator.cs |
1.419 ms frente a 711 ms para 667.125 voicings | Corregido upstream por #730: los productores hacen ese trabajo, 649-679 ms frente a 786-861 ms en secuencial, medido para la PR; no vuelto a medir en este curso 2026-09-17, 2026-09-24 |
PitchClassSet.GetCompatibleKeys es lo bastante barato para llamarlo por voicing |
Reconstruía las 30 tonalidades y sus conjuntos de clases de altura en cada llamada | PitchClassSet.cs |
13,5 % del perfil del análisis; 180,5 ms y 110.116 bytes por llamada en 4.096 llamadas | Corregido upstream por #730: una prueba de inclusión sobre máscaras de 12 bits, calculada una vez por conjunto, 0,1 ms y 0 bytes; no vuelto a medir en este curso 2026-09-17, 2026-09-24 |
ValueObjectUtils<TSelf>.Items devuelve la colección que guarda |
Crea una ValueObjectCollection<TSelf> nueva de 32 bytes en cada llamada |
ValueObjectUtils.cs |
32 bytes por llamada | Corregido upstream por #730: una sola colección inmutable, 0 bytes por lectura 2026-09-16, 2026-09-24 |
IRangeValueObject<TSelf>.EnsureValueInRange normaliza sobre el rango real |
Normaliza con un tamaño de rango desviado en uno | IRangeValueObject.cs |
Valor equivocado en el límite | Corregido upstream en #598, fusionada el 2026-09-24, sin prueba; #731 añade una, que falla con el código antiguo 2026-09-16, 2026-09-24 |
default(Str) da un objeto de valor válido |
Da la cuerda 0, que la propia comprobación de rango de Str prohíbe — igual que new Str[n] y new T() en un método genérico |
todo objeto de valor de GA cuyo mínimo es 1 | La lección 5 muestra las tres puertas | Reproducido, límite del patrón más que defecto 2026-09-16 |
El out de IStaticReadonlyCollection<out TSelf> aporta varianza |
No aporta nada: 13 de sus 17 implementaciones son structs, y la interfaz no tiene miembros de instancia | IStaticReadonlyCollection<out TSelf> |
13 de 17 | Reproducido, inocuo 2026-09-16 |
ValueObjectCache<T> construye lo que sus llamadores usan |
Construye en su primer uso dos FrozenSet que nadie lee |
ValueObjectCache.cs |
Dos conjuntos construidos, cero lectores | Corregido upstream por #730: los dos conjuntos se construyen en el primer uso 2026-09-16, 2026-09-24 |
| El calentamiento de un benchmark mide lo que miden sus iteraciones | El mismo código midió 31 ns en el calentamiento y de 41 a 52 ns en las iteraciones | BenchmarkDotNet, VoicingWithSpans |
Fijado a un núcleo con --affinity mide 32,4 ns, en el primer núcleo igual que en el último: los núcleos del Core Ultra 9 285K no son iguales |
Entendido, y por eso la lección fija el núcleo 2026-09-14 |
Añadir [MemoryDiagnoser] a una serie en marcha añade su columna |
Sin recompilar, la serie usó el build anterior y no imprimió ninguna columna Allocated, sin avisar |
BenchmarkDotNet | Sin columna, sin aviso | Reproducido, un error del usuario que la herramienta vuelve invisible 2026-09-14 |
| El PGO dinámico elimina la asignación del enumerador como en la lección 4 | No: la primera versión con máscaras seguía asignando 72 bytes por llamada, de enumeradores obtenidos por IEnumerable<int> |
PGO dinámico de .NET 10 | Pasar a int[] y HashSet<int> quitó los 72 bytes y lo hizo 2,7 veces más rápido todavía |
Reproducido; por qué el caso de la lección 4 se desvirtualiza y este no sigue abierto 2026-09-17 |
PitchClassSetId.ItemsSpan devuelve una vista de los ids |
Copiaba los 4.096 ids en un array nuevo en cada llamada: Items es una lista de solo lectura generada por el compilador, así que la prueba is PitchClassSetId[] siempre era falsa |
PitchClassSetId.cs#L59-L71 |
16.408 bytes por llamada | Corregido upstream por #687, fusionada el 2026-09-20; no se volvió a medir 2026-09-14, 2026-09-24 |
El operador - de PitchClass cuesta más o menos lo que cuesta (a - b + 12) % 12 |
Buscaba el resultado en un FrozenDictionary de 144 tuplas |
PitchClass.cs#L110-L137 |
583 ns para los 144 pares, frente a 134 ns para la aritmética y 41,5 ns para un array plano | Corregido upstream por #687 con una tabla plana de 144 entradas; no se volvió a medir 2026-09-14, 2026-09-24 |
Se puede bloquear sobre Try.OfAsync, y cancelarlo |
Esperaba sin ConfigureAwait(false), así que bloquear sobre él bajo un contexto de sincronización de un solo hilo producía un interbloqueo, y convertía OperationCanceledException en un fallo corriente |
Try.cs#L62-L73 |
Mostrado en la lección 3 | Corregido upstream por #687 2026-09-14, 2026-09-24 |
LazyWithExpiration<T> no cuesta nada mientras espera |
Ocupaba un hilo del pool en Thread.Sleep por valor |
LazyWithExpiration.cs#L20-L40 |
64 valores que expiraban tras un segundo retrasaban un Task.Run ajeno 2 s en mi máquina y de 10 a 12 s en los runners de 3 y 4 núcleos |
Corregido upstream por #687: la expiración es una marca de tiempo comprobada en cada acceso 2026-09-14, 2026-09-24 |
Experimentos
Sección titulada «Experimentos»El trabajo de rendimiento invita a leer una predicción dentro de su propio resultado, así que estas filas conservan la hipótesis tal como se escribió y la dejan equivocarse. Dos de las seis primeras están refutadas, y el método del apéndice 2 — comparar dos builds de GA en lugar de dos métodos, y comprobar que cada respuesta es idéntica byte a byte — es él mismo la tercera fila. Las cinco filas del apéndice 4 se cumplieron todas, incluida la última, escrita para poner a prueba una explicación y no un comportamiento.
| Pregunta | Hipótesis | Resultado | Veredicto | Dónde |
|---|---|---|---|---|
¿Es FrozenDictionary más rápido que Dictionary con doce sufijos de acorde cortos? |
Escrita de antemano: sí, para eso está | No lo era | Refutada | 2026-09-14 |
¿Es SearchValues más rápido que un bucle sobre símbolos de acorde de 1 a 6 caracteres? |
Escrita de antemano: sí | 1,7 veces más lento | Refutada | 2026-09-14 |
¿Da un build con los cambios de análisis respuestas idénticas byte a byte a main? |
Escrita de antemano, y es el método entero: la demostración compara dos builds de GA, no dos métodos | Idénticas en los cinco volcados: TryMatch 258 048 líneas, reconocimiento 106 496, vectores de clases de intervalo 8192, los 667 125 voicings, 2048 búsquedas sobre el índice real de 313 047 entradas |
Confirmada | 2026-09-17 |
¿Venían de Parallel.For los 38 MB por consulta de la búsqueda? |
Escrita de antemano: el paralelismo era el sospechoso | Un barrido secuencial asignó los mismos 38 MB, lo que absolvió a Parallel.For y señaló a GetVector |
Refutada | 2026-09-17 |
| ¿Sobrevive el particionado por rangos de la búsqueda al arreglo de asignaciones? | No escrita como predicción: medía un 8 % más rápido antes del arreglo, así que parecía digno de conservar | Un 22 % más lento después. Descartado | Refutada | 2026-09-17 |
| ¿Se acelera la exportación OPTIC-K entera de punta a punta, y no solo el benchmark? | Escrita de antemano: el análisis es la mayor parte de la exportación | Cuatro ejecuciones alternando main y el build modificado: 142,8 s frente a 62,9 s, luego 95,0 s frente a 38,3 s. El mismo binario de main tardó 142,8 s y luego 95,0 s: la máquina no estaba ociosa, y son los pares, no los tiempos absolutos, el resultado |
Confirmada | 2026-09-17 |
| ¿Pueden dos módulos ejecutar dos versiones de Newtonsoft.Json en un mismo proceso? | No escrita como predicción: vista primero en un prototipo | 13.0.3 y 13.0.1 lado a lado, ambas con la versión de ensamblado 13.0.0.0 | Confirmada | 2026-10-22, Plugins/ |
| ¿Puede el módulo B usar un servicio del módulo A sin referenciar A? | Escrita de antemano: sí si la interfaz está en los contratos; sin A, ValidateOnBuild falla al construir el contenedor |
B invierte el deletreo de A; sin A, «Unable to resolve service for type ‘Contracts.IChordSpeller’» desde BuildServiceProvider |
Confirmada | 2026-10-22 |
| ¿Un ensamblado cargado en dos contextos da un solo tipo? | Escrita de antemano: no, dos tipos con un mismo nombre, y un solo IModule compartido |
Mismo nombre True, mismo tipo False; el IModule que implementan ambos es el del host |
Confirmada | 2026-10-22 |
¿Se descarga todavía un módulo cuya herramienta hace correr un Timer? |
Escrita de antemano: sí una vez liberado el contenedor, nunca cuando se abandona | 1 de 1 liberado; 0 de 1 abandonado, tras 100 recolecciones, en cada ejecución. El control sin temporizador: 2 de 2 abandonado | Confirmada | 2026-10-22 |
| ¿Es lo que captura el callback del temporizador lo que retiene el módulo C? | Escrita tras el resultado de la fila anterior, antes del código del módulo D: un callback estático deja descargarse al módulo, ya que un Timer activo puede recolectarse |
El módulo D se descarga, 1 de 1, pero en diez recolecciones seguidas solo 36 veces de 40; con 10 ms entre recolecciones, tres cada vez | Confirmada | 2026-10-22 |
2026-09-14 — Configuración y la versión fijada de GA
Sección titulada «2026-09-14 — Configuración y la versión fijada de GA»- El curso se compila con tres proyectos de GA,
GA.Core,GA.Domain.CoreyGA.Business.Config, descargados porfetch-ga.shdesde el commita826864, la cabeza de la ramamainde GA ese día, con un clon disperso y sin blobs: unos 11 MB en lugar del repositorio entero. Es el mismo commit y el mismo script que los del curso de teoría musical, así que ambos cursos leen el mismo código. - Mi máquina tiene el SDK de .NET 10.0.112 y una preview de 11.0. El
global.jsondel curso fija10.0.100conrollForward: latestFeature, así quedotnetelige 10.0.112 en la carpeta del curso y la preview fuera de ella. GA.Corehace referencia al paqueteMicrosoft.Net.Compilers.Toolseten su versión 4.11.0: ese proyecto lo compila el Roslyn 4.11 del paquete, no el Roslyn 5.0 del SDK. Compila sin problemas; no he buscado diferencias en el IL que produce, por verificar.ilspycmd11.0.0.9375 es una herramienta local, restaurada porcheck.sh. Dos sorpresas:-ildesensambla el ensamblado entero e ignora-t Type, así quecheck.shdesensambla una sola vez el ensambladoSnippets; y el IL contiene comentarios// Method begins at RVA 0x…, que cambian con cada modificación de un método no relacionado, así quecheck.shlos elimina antes de comparar.- Descompilar con
-lv CSharp4muestra la máquina de estados asíncrona: en el nivel de lenguaje C# 4, ILSpy no puede volver a convertirla enawait.
2026-09-14 — Comprobar los errores del compilador
Sección titulada «2026-09-14 — Comprobar los errores del compilador»- Los fragmentos rechazados se compilan en memoria con un pequeño programa Roslyn,
CompileFail, en lugar de con un proyecto por fragmento: una sola compilación en lugar de once. Añade losusingimplícitos del SDK como un árbol de sintaxis aparte y hace referencia a los ensamblados del runtime. Sus mensajes son idénticos a los dedotnet build. - Faltaba CS4007 al principio: el comprobador llamaba a
compilation.GetDiagnostics(), y unSpan<int>usado a través de unawaitcompilaba sin error. Ese error solo se notifica mientras el compilador reescribe el método como máquina de estados, cosa queGetDiagnostics()no hace. El comprobador llama ahora acompilation.Emit(Stream.Null)y lee sus diagnósticos. - Esperaba CS8175 para un span capturado por una lambda, por lo que recordaba de compiladores más antiguos; Roslyn 5.0 notifica CS9108 («Cannot use parameter ‘frets’ that has ref-like type inside an anonymous method…»). La lección 1 cita el mensaje real.
- El fragmento de CS8425 produce una advertencia, no un error: el comprobador la espera y falla si no aparece.
2026-09-14 — Lo que hizo el runtime y el IL no dice
Sección titulada «2026-09-14 — Lo que hizo el runtime y el IL no dice»- Encapsulado (boxing) eliminado por el JIT. Un
boxseguido deunbox.anydel mismo tipo no asigna nada, ni siquiera en el nivel 0. ConDOTNET_TieredCompilation=0, una caja que no escapa del método se asigna en la pila, también cuando el valor se convierte a una interfaz: es la asignación de objetos en la pila de .NET 9, ampliada en .NET 10.check.shejecuta la lección 1 de las dos maneras. - El umbral del montón de objetos grandes se comprueba sobre el tamaño antes de redondear:
byte[84,975]cuesta 85 000 bytes y se queda en la generación 0,byte[84,976]cuesta lo mismo y va al LOH (lección 2). - Los literales están en un montón congelado:
GC.GetGeneration("C major")yGC.GetGeneration(typeof(PitchClass))devuelvenint.MaxValue. - Una primera versión del bucle de presupuesto de asignación de la lección 2 guardaba sus arrays en una variable local, y el JIT podía asignarlos en la pila después de OSR, sin dejar basura que recolectar. Ahora los arrays escapan a un anillo de 16.
Task.WhenAllconserva las excepciones internas en el orden en que fallaron las tareas, que cambia entre ejecuciones: el programa las ordena, e imprime el orden real en una línea que depende de la máquina.SearchValues.Create("#b")devuelveAny2CharPackedSearchValuesen x64 yAny2SearchValues`2en Arm64 (el runner de macOS): otra línea que depende de la máquina.- El presupuesto de gen0 depende del tamaño de la caché de la máquina: 18 MB en mi máquina, 16 MB en el runner de Linux, 24 MB en el de Windows y 6 MB en el de macOS, donde los mismos 32 MB de basura provocaron cinco recolecciones en lugar de una.
2026-09-14 — Benchmarks
Sección titulada «2026-09-14 — Benchmarks»- BenchmarkDotNet 0.15.8. Máquina: Intel Core Ultra 9 285K (24 núcleos), 64 GB, Windows 11 25H2, .NET SDK 10.0.112, runtime 10.0.12. BenchmarkDotNet cambió el plan de energía de Windows a Alto rendimiento en cada ejecución, y lo restauró después.
- La CI ejecuta cada benchmark con
--job Dry. La primera ejecución de prueba tardó 8 min 30 s:JitBenchmarksdeclara sus cuatro jobs en una configuración, y--job Dryañadió un job de prueba a esos cuatro en lugar de sustituirlos. La configuración usa ahoraJob.DrycuandoBENCHMARKS_DRY=1, y toda la ejecución de prueba tarda unos 30 s en local, y entre 17 y 53 s en los runners. - La ejecución completa de las siete clases, una tras otra y sin nada más en marcha, tardó unos 45 minutos.
JitBenchmarkspor sí sola ejecuta cuatro jobs y tardó 4 minutos. - El mismo código midió 31 ns en el calentamiento y entre 41 y 52 ns en las iteraciones reales (
VoicingWithSpans). Fijado a un núcleo con--affinity, midió 32.4 ns, tanto en el primer núcleo como en el último. El Core Ultra 9 285K tiene núcleos de rendimiento y de eficiencia; no he comprobado de qué tipo son esos dos núcleos, ni he confirmado que la migración de subprocesos explique las iteraciones más lentas, por verificar. La lección 4 cuenta la historia y conserva las tablas sin fijar, ya que es lo que obtendrá un lector por defecto. - Añadí
[MemoryDiagnoser]aJitBenchmarksmientras se ejecutaban las demás clases, sin recompilar: la ejecución usó la compilación anterior y no imprimió ninguna columnaAllocated. Tras recompilar y volver a ejecutar, mostró el resultado en el que se basa la lección 4: con PGO, el bucle sobreIEnumerable<int>ya no asigna su enumerador de 40 bytes, y es 9 veces más rápido. - Dos de mis expectativas eran erróneas:
FrozenDictionaryno fue más rápido queDictionarycon doce sufijos de acorde cortos, ySearchValuesfue 1.7 veces más lento que un bucle con símbolos de acorde de 1 a 6 caracteres. Ambos resultados aparecen en la lección 4 tal como se midieron.
2026-09-14 — Dogfooding: lo que las lecciones encontraron en GA
Sección titulada «2026-09-14 — Dogfooding: lo que las lecciones encontraron en GA»Todavía no he comunicado ninguno de estos hallazgos al proyecto GA; los enumero para que sus mantenedores decidan.
PitchClassSetId.ItemsSpancopia los 4096 identificadores en un array nuevo en cada llamada, 16 408 bytes.Itemsestá declarado comoIReadOnlyCollection<PitchClassSetId>e inicializado con una expresión de colección, así que el compilador crea un<>z__ReadOnlyList<PitchClassSetId>, y la pruebais PitchClassSetId[]que debería evitar la copia siempre es falsa (lecciones 1 y 2).- El operador
-dePitchClassbusca en unFrozenDictionaryde 144 tuplas para calcular(a - b + 12) % 12. Tarda 583 ns para los 144 pares, frente a 134 ns de la aritmética y 41.5 ns de un array plano de 144 valores (lección 4). Try.OfAsyncespera sinConfigureAwait(false), así que bloquear esperándolo bajo un contexto de sincronización de un solo subproceso provoca un interbloqueo, y además capturaOperationCanceledException, con lo que convierte una cancelación en un fallo ordinario (lección 3).LazyWithExpiration<T>mide su caducidad con unThread.Sleepen un subproceso del grupo de subprocesos, uno por valor. Con 64 valores que caducan al cabo de un segundo, unTask.Runsin relación esperó 2 s en mi máquina y entre 10 y 12 s en los runners de 3 y 4 núcleos (lección 3). No he buscado los sitios en los que GA lo usa, por verificar.
2026-09-14 — CI
Sección titulada «2026-09-14 — CI»- Commit
d85a319, ejecución 34915741516: en verde en los tres sistemas operativos, solo el código de las lecciones. - Commit
8ba378e, ejecución 34919815590: las soluciones de los ejercicios, en verde en los tres sistemas operativos. Los jobs tardaron 1 min 23 s en Linux, 2 min en macOS y 3 min 14 s en Windows, de los cuales la ejecución de prueba de los 37 benchmarks ocupó 23 s, 27 s y 53 s. Unos 14 s de cada job corresponden a la demostración deLazyWithExpirationde la lección 3: unTask.Runesperó 11 001 ms en Linux, 11 766 ms en macOS y 11 050 ms en Windows, donde la ejecución anterior había medido 9878 ms. - El runner de Windows dio los mismos anchos de vector y las mismas diferencias en coma flotante que mi máquina; el runner de Linux tiene AVX-512.
2026-09-15 — Un nuevo plan, y empieza la parte 2
Sección titulada «2026-09-15 — Un nuevo plan, y empieza la parte 2»- El curso pasa de 12 a 24 lecciones en cuatro partes: runtime y rendimiento, concurrencia y flujo de datos, ASP.NET Core en profundidad, metaprogramación y herramientas. Las lecciones 1 a 4 conservan sus slugs; las antiguas lecciones 6 y 12 pasan a ser las lecciones 10 y 19, y las lecciones 2 y 3 apuntan ahora a ellas. Las lecciones 6 a 9 se escribieron antes que la lección 5.
- Las partes 2 y 3 comparan cada tema con Spring y Reactor, de C# a Java. El curso de Spring Boot, Spring Cloud y Reactor hace la comparación en el otro sentido, así que las lecciones enlazan con sus páginas en lugar de volver a explicar Reactor, y la parte 3 construirá el equivalente en ASP.NET Core de su servicio de escalas.
2026-09-15 — Volver deterministas los programas concurrentes
Sección titulada «2026-09-15 — Volver deterministas los programas concurrentes»Cada comportamiento de las lecciones 6 a 9 lo imprime el programa y se compara con expected/, en tres sistemas operativos. Una primera versión de cada lección imprimía algo que cambiaba entre ejecuciones; cada lección se ejecutó al menos nueve veces seguidas antes de hacer commit de su salida.
- Barreras, no retardos. Los elementos se retienen con un
TaskCompletionSourcehasta que el programa ha visto lo que quiere mostrar, y «el productor está atascado» se mide esperando a que un contador deje de moverse, e imprimiendo después el contador. - Las continuaciones se ejecutan cuando quieren. Un
WriteAsync(...).AsTask()que había terminado seguía indicandoIsCompleteden false en algunas ejecuciones, porque su continuación es asíncrona: el programa ahora lo espera. Lo mismo conFaultsobre un bloque de Dataflow, cuyoCompletion.Exceptionseguía siendonulljusto después de la llamada. EnsureOrdered = falseno significa «al revés». Una primera prueba esperaba que el lento elemento 0 saliera el último; no lo hizo en 8 ejecuciones de 20. La lección 7 muestra ahora lo que un consumidor puede recibir mientras el elemento 0 se ejecuta.- Límites en tiempo virtual. Las notas en exactamente 500 o 1.000 ms caían en el borde de las ventanas de
SampleyBuffer; las notas de la lección 8 están colocadas lejos de ellos. Reader.CountlanzaNotSupportedExceptionen un canal no acotado creado conSingleReader = true: suCanCountesfalse. El programa vacía el canal para contar lo que queda.- Rx.NET 7.0 no tiene puente hacia
IAsyncEnumerable:ToAsyncEnumerable()sobre un observable no compiló. La lección 9 escribe a mano las dos direcciones. - La primera versión de la lección 7 etiquetaba Dm7 como Forte 4-3, a partir de
ProgrammaticForteCatalogde GA; la tabla de Forte dice 4-26. La lección toma ahora las etiquetas deCanonicalForteCataloge imprime ambas. - Commits
69bb214yb4d713f, ejecuciones 34994143077 y 34995561655: en verde en los tres sistemas operativos. - El benchmark de flujos de la lección 9 se ejecutó solo durante 5 minutos en la misma máquina que la lección 4. BenchmarkDotNet indicó una distribución bimodal para
RxObserveOnTaskPool.
2026-09-15 — Dogfooding: canales, Dataflow y Rx en GA
Sección titulada «2026-09-15 — Dogfooding: canales, Dataflow y Rx en GA»GA usa canales en su generador de voicings y en su comando de indexación, y TPL Dataflow y Rx.NET en una demo de rendimiento. Nada de esto se ha notificado todavía aguas arriba.
VoicingGenerator.GenerateAllVoicingsAsync: un consumidor que se detiene antes de tiempo, como el.Take(100)del ejemplo de uso, deja a los productores generando todas las ventanas en un canal no acotado; una ventana que lanza una excepción deja al consumidor esperando para siempre, porque nunca se llega aWriter.Complete(); el resumen dice que el orden se conserva, los comentarios de debajo dicen que no (lecciones 6 y 9).IndexVoicingsCommand: cuando el consumidor falla, por ejemplo porque la base de datos está caída, registra el error y retorna, y los productores esperan para siempre ante el canal lleno (lección 6). El canal acotado en modoWaites la elección correcta.PerformanceOptimizationDemo: la parte de Dataflow cuenta los resultados de unaList<T>que llena otro subproceso sin esperar a ese subproceso, e ignora el resultado deSendAsync(lección 7); la parte Rx cuenta lotes como eventos, 10 en lugar de 1.000, y espera 500 ms fijos en lugar de esperar el pipeline (lección 8). Su parte de canales es correcta.- La misma demo y
MusicalAnalysisApp, ambosnet10.0, hacen referencia al paqueteSystem.Threading.Tasks.Dataflow9.0.10, que .NET 10 ya tiene en su framework compartido. Un proyectonet10.0nuevo con la misma referencia recibe la advertencia NU1510 al restaurar, y carga de todos modos el ensamblado del framework. - Los números de
ProgrammaticForteCatalogsiguen otro orden que la tabla de Forte, aunque sus observaciones dicen que las diferencias son menores: 4-3 para el acorde de séptima menor donde Forte dice 4-26 (lección 7). - GA tiene varias clases
BackgroundService, entre ellas el precalentamiento de la caché y la inicialización del índice de voicings; la lección 15 es el lugar para leerlas.
2026-09-15 — Apéndice 1: optimizar GA, primero la demostración
Sección titulada «2026-09-15 — Apéndice 1: optimizar GA, primero la demostración»La lección 4 medía el código de GA tal cual. Este apéndice reescribe tres de sus miembros, y lo interesante resultó no ser la ganancia de velocidad sino lo que la reescritura no tiene permitido cambiar.
- Los tres son
PitchClassSetId.IsClusterFree,PitchClassSet.IntervalClassVectoryPitchClassSet.ClosestDiatonicKey. Todos reciben un conjunto de 12 bits, así que el dominio de entrada entero tiene 4096 valores:Advanced -- a1compara cada reescritura con la respuesta de GA para cada uno de ellos, y la CI lo ejecuta en tres sistemas operativos. Escribir la demostración antes que el benchmark cambió lo que estaba dispuesto a afirmar. IntervalClassVectorIdempaqueta seis cuentas como dígitos en base 12, y las cuentas de 12 del agregado cromático acarrean. GA lo documenta como una limitación conocida. Empaquetarlo correctamente cambiaría el identificador del conjunto 4095, yProgrammaticForteCatalogordena cada cardinalidad por ese identificador, así que todos los números de Forte podrían moverse — la versión rápida reproduce el acarreo en su lugar. Una «corrección» colada dentro de un cambio de rendimiento es justamente de lo que trata este apéndice.- La respuesta de
ClosestDiatonicKeydepende de queOrderByDescendingsea estable: los empates van a la tonalidad queKey.Itemslista primero, las 15 mayores antes que las 15 menores. Un bucle que sustituyera al que va ganando con>=en lugar de>devolvería en silencio otra tonalidad en cada empate; el que sustituye con>coincide con GA en los 4096 conjuntos. - Los primeros benchmarks estaban mal, y halagaban. Llamar a cada miembro una vez sobre un argumento
constdejó que el JIT plegara la llamada en un literal: elIsClusterFreerápido salía a 0,0107 ns, una veinticincoava parte de un ciclo. Un estático mutable eliminó el plegado y seguía dando una mediana de cero, porque BenchmarkDotNet resta un método vacío. Reescritos para barrer los 4096 conjuntos,IsClusterFreees 4 veces más rápido, no 31 — y yo habría publicado el 31. - El hallazgo son los 175 KB, no los microsegundos.
IdentifyClosestKeyrecibe unDictionary<Key, IReadOnlyCollection<PitchClass>>y lo desestructura comoforeach (var (key, _) in items): los valores se construyen para las 30 tonalidades y nunca se leen. No hizo falta ningún perfilador — bastó con leer el método. ToNormalFormyPrimeFormtambién están hechos, así que el apéndice demuestra ahora cinco miembros y no tres. Tabular la forma normal quitó los últimos 16 KB delClosestDiatonicKeyrápido, que ya no asigna nada: 1.615 veces más rápido, 175 KB por llamada eliminados.PrimeForm, que ya era aritmética de bits, solo dio 1,8 — el techo honesto de reescribir aritmética correcta, y merece estar al lado del 1.615.- Nada de esto se ha reportado aún aguas arriba.
2026-09-16 — Lección 5: genéricos en profundidad
Sección titulada «2026-09-16 — Lección 5: genéricos en profundidad»- La lección 5 llena el hueco entre las lecciones 4 y 6, que se escribieron antes. Sus salidas proceden de la misma máquina y del mismo SDK que el resto del curso; commit del código
378eec1. - Ejecución de CI 35173274425: verde en Linux, Windows y macOS. Las líneas que dependen de la máquina fueron las mismas en los tres runners que en mi máquina, incluido el de Arm64: 4592 bytes en el primer acceso a la caché de
Str, y 1 y luego 0 métodos compilados paraShared<string>y luegoShared<object>. - El propio resumen del JIT como prueba.
DOTNET_JitDisasmSummary=1yDOTNET_JitStdOutFilefuncionan en el runtime publicado, así quecheck.shcompara la lista de compilaciones deShared<T>.Describe: cuatro para seis argumentos de tipo, una de ellas sobreSystem.__Canon. LanzaAdvanced.dlldirectamente: a través dedotnet run, el propio proceso del SDK heredaría las variables y escribiría en el mismo archivo. - Lo que el runtime no comprueba.
MakeGenericMethodaceptó(int, string)para un parámetrowhere T : unmanaged; el compilador lo rechaza con CS8377.notnullno deja nada enGenericParameterAttributes. - Un fragmento que esperaba que fallara compiló.
IStaticReadonlyCollectionFromValues<TSelf>de GA oculta elItemsabstracto estático con una propiedadnew staticque tiene cuerpo. Esperaba queT.Items, sobre un parámetro de tipo restringido a esa interfaz, se rechazara; Roslyn 5.0 lo compiló. Quité el fragmento; a qué miembro se enlaza esa llamada queda por verificar. - El desensamblado cambió mi lectura del benchmark. En el código compartido, leer un campo estático de
Counter<T>llama aCORINFO_HELP_GET_NONGCSTATIC_BASEen cada iteración, tanto en el nivel 1 como con la compilación por niveles desactivada, y aun así el bucle solo fue 1.46 veces más lento que la versión paraint. - De nuevo la PGO. Un
foreachsobrePitchClass.Itemsasigna 72 bytes en las primeras llamadas del programa y 40 bytes en el benchmark, después de la PGO dinámica.
2026-09-16 — Dogfooding: las interfaces de objetos de valor de GA
Sección titulada «2026-09-16 — Dogfooding: las interfaces de objetos de valor de GA»Nada de esto se ha notificado todavía aguas arriba.
ValueObjectUtils<TSelf>.Itemscrea unValueObjectCollection<TSelf>nuevo de 32 bytes en cada lectura, aunque la interfaz documentaItemscomo memoizado; unforeachsobre él asigna 72 bytes (lección 5).Valuesestá declarado comoIReadOnlyList<int>enIStaticValueObjectList<TSelf>, y las implementaciones devuelven unImmutableArray<int>: 24 bytes de boxing por lectura. Doce lecturas tardaron 41.8 ns y asignaron 288 bytes, frente a 3.1 ns y nada a través delImmutableArray(lección 5).ValueObjectCache<T>construye en su primer uso dosFrozenSetque nada lee en los tres proyectos descargados: 4592 bytes para los 26 valores deStr(lección 5).IRangeValueObject<TSelf>.EnsureValueInRangenormaliza con un tamaño de rango demax - minen lugar demax - min + 1: 12 se convierte en 2 para una clase de altura. Ningún llamador de los proyectos descargados pasanormalize: true, así que el error está latente (lección 5).default(Str),new Str[n]ynew T()dan la cuerda 0, que la comprobación de rango deStrprohíbe; lo mismo vale para todos los objetos de valor de GA cuyo mínimo es 1 (lección 5).- El
outdeIStaticReadonlyCollection<out TSelf>no tiene ningún efecto: 13 de sus 17 implementaciones son structs, y la interfaz no tiene miembros de instancia (lección 5).
2026-09-17 — Apéndice 2: perfilar GA, con pull requests
Sección titulada «2026-09-17 — Apéndice 2: perfilar GA, con pull requests»El apéndice 1 eligió qué optimizar leyendo GA. Esta vez eligió un perfilador, sobre el propio pipeline de GA en el commit 66bdd04, y cada cambio se envió aguas arriba. El trabajo fue de la noche del 2026-09-16 a la madrugada del 2026-09-17.
-
Tres pull requests.
- #695: el reconocimiento de acordes con máscaras de 12 bits, calculado una sola vez por conjunto de clases de altura.
- #694: el vector de clases de intervalo, calculado una sola vez por conjunto.
- #693:
OptickIndexReader.Dimension, leído una sola vez.
Cada una se hizo en su propia rama a partir de
main. Las pruebas de GA pasaron antes de abrir cada PR: GA.Domain.Core.Tests 458/458 y 459/459, y las pruebas de acordes y voicings de GA.Business.Core.Tests 421/421 y 419/419. En GA.Business.ML.Tests, las pruebas de búsqueda y de esquema pasaron 227 de 228; la que falla lo hace de la misma manera enmain. -
Adónde iba el tiempo.
dotnet-tracesobre el análisis de voicings situó alrededor del 52 % del subproceso principal enCanonicalChordRecognizer.IdentifyChordSet, la mayor parte construyendoHashSet. Un sondeo dio 173 µs y 347 KB por voicing paraVoicingAnalyzer.Analyze, y 38 MB por consulta para una búsqueda OPTIC-K. -
La demostración compara dos compilaciones de GA, no dos métodos. Un único programa de volcado se compila contra
mainy contra la rama, escribe cada respuesta, ycmpcompara los archivos. Todos fueron idénticos byte a byte:TryMatch: 258.048 líneas;- reconocimiento: 106.496 líneas, cada conjunto × 13 bajos × 2 pasadas;
- vectores de clases de intervalo: 8.192 líneas;
- los 667.125 voicings de guitarra a través de
VoicingAnalyzer.Analyze; - 2.048 búsquedas sobre el índice real de 313.047 entradas.
La demostración propia del curso,
GaPerf -- a2, comprueba 6.856.704 llamadas aTryMatchy 106.496 reconocimientos, y se ejecuta en la CI. -
El hallazgo fue la caché, no las máscaras. Los 667.125 voicings usan unos 2.500 conjuntos de clases de altura distintos, así que la búsqueda de patrones de cada conjunto se repetía 266 veces. Con un array de 4096 casillas, el reconocimiento es 7.600 veces más rápido en el benchmark del curso. En GA, con los dos cambios,
VoicingAnalyzer.Analyzepasó de 234,34 ms y 759 MB a 8,94 ms y 22 MB para 2.000 voicings. -
Los 38 MB de la búsqueda no estaban en la búsqueda. Un experimento con un recorrido secuencial asignó los mismos 38 MB que el paralelo, lo que descartó a
Parallel.For. Los bytes venían deGetVector, que leía una propiedad que ejecuta unWhere+Sumde LINQ sobre el registro de particiones: 626.094 consultas por búsqueda. Leída una sola vez, una búsqueda pasó de 4,986 ms y 38,25 MB a 2,162 ms y 41 KB. -
Dos resultados que no esperaba.
- La primera versión con máscaras seguía asignando 72 bytes por llamada, por los enumeradores obtenidos a través de
IEnumerable<int>. Distinguirint[]yHashSet<int>con unswitchlos eliminó, y la hizo otras 2,7 veces más rápida. La PGO dinámica no eliminó esta asignación, a diferencia de la de la lección 4; por qué no queda por verificar. - El particionado por rangos de la búsqueda, que parecía un 8 % más rápido antes de la corrección, resultó un 22 % más lento después. Lo descarté.
- La primera versión con máscaras seguía asignando 72 bytes por llamada, por los enumeradores obtenidos a través de
-
De extremo a extremo, la exportación OPTIC-K se ejecutó cuatro veces, alternando el
mainde GA y una compilación con los dos cambios de análisis: 142,8 s frente a 62,9 s, y después 95,0 s frente a 38,3 s. Los archivos de índice fueron idénticos entrada a entrada. -
La máquina no estaba inactiva, y las cifras lo dicen.
- Había otras sesiones en marcha. Cada compilación y cada serie de benchmarks tomó un bloqueo global de la máquina, y cada serie de BenchmarkDotNet tomó además el bloqueo de la GPU.
- Al principio de cada serie registré la RAM libre y los procesos más activos: entre 13,6 y 18,5 GB libres, entre el 32 % y el 100 % de CPU total, con Microsoft Defender, Docker y WSL a la cabeza.
- El mismo binario de exportación tardó 142,8 s, y luego 95,0 s.
- El coordinador informó de dos capturas de Chrome sin interfaz hechas por otra sesión entre las 00:47 y las 00:49. Ninguna de mis series se ejecutó en esa ventana: los benchmarks anteriores terminaron a las 00:42:38, y el siguiente empezó a las 00:50:22. Aun así, volví a ejecutar las series de GA que acababan de correr justo antes, con las mismas asignaciones y tiempos dentro de las barras de error.
- Las tablas de antes y después de GA usan
ShortRun. Sus asignaciones son exactas; los tiempos, orientativos. - Una ejecución de la serie de acordes no llegó a hacerse: mi script apuntaba a un worktree que no existía, y la volví a lanzar.
-
Medido, y sin cambiar.
- El camino paralelo de
VoicingGeneratores el doble de lento que el secuencial (1.419 ms frente a 711 ms para 667.125 voicings), yPitchClassSet.GetCompatibleKeysocupa el 13,5 % del perfil del análisis. Los dos archivos pertenecen a correcciones de GA en curso en otra sesión, así que estos hallazgos fueron allí como propuestas. KeyIdentificationService.Identifyno está en el camino caliente: 25 µs, una vez por petición.- Las asignaciones de los objetos de valor de la lección 5 no aparecen en este perfil.
- El camino paralelo de
-
La exportación del índice no siempre es reproducible. La primera exportación de la noche difiere de las posteriores en 266 de las 313.047 entradas, identificadas por instrumento y diagrama, aunque salió del mismo commit de GA. Todas las comparaciones de arriba se hacen entre exportaciones del mismo grupo; la causa queda por verificar.
2026-09-21 — Lecciones 10-14: concurrencia y ruta de petición ASP.NET Core
Sección titulada «2026-09-21 — Lecciones 10-14: concurrencia y ruta de petición ASP.NET Core»- Cinco lecciones ejecutables en .NET 10.0.12: lost update determinista,
Lock,Interlocked,ConcurrentDictionaryyParallel.ForEachAsynclimitado. - Servidores Kestrel reales en puertos loopback efímeros; se observaron ciclo de vida, petición, apagado graceful, orden de middleware y short-circuit
429. - Se verificaron lifetimes singleton/scoped, rechazo de captive dependency, keyed services y validación de options.
- Se verificaron una Minimal API, un controlador en la misma tabla de routing y una respuesta JSON
IAsyncEnumerable<string>. - Las salidas portables están en
expected/l10.txtaexpected/l14.txt; los mínimos del thread pool dependen de la máquina. - Son pruebas locales: buffering de proxy, límites de producción, identity provider real y starvation ante una dependencia remota quedan por medir.
2026-09-21 — Lecciones 15-16: trabajo hospedado y autorización local
Sección titulada «2026-09-21 — Lecciones 15-16: trabajo hospedado y autorización local»- La prueba de servicios hospedados usa barreras de terminación en lugar de demoras. Un canal limitado alimenta un worker singleton y dos operaciones resuelven instancias scoped distintas (
scope-1yscope-2). La cancelación del host se observa mientras el worker espera más trabajo. - Un segundo host libera un fallo deliberado y observa
ApplicationStoppingconBackgroundServiceExceptionBehavior.StopHost. Esto demuestra la política del host, no reintento ni recuperación. - La prueba de autenticación inicia Kestrel en un puerto loopback efímero. Los bearer tokens ausente, mal formado, caducado y con firma incorrecta devuelven 401; una identidad válida sin
scope=scales.readdevuelve 403; la identidad autorizada recibe200 C major. - Ambas claves se generan en memoria para un solo proceso. La vida útil usa un instante fijo y ninguna clave ni token se imprime ni persiste.
Advancedcompiló localmente sin advertencias y las dos salidas nuevas coinciden conexpected/l15.txtyexpected/l16.txt. Quedan por verificar la CI en tres sistemas, un servidor de autorización real, descubrimiento/rotación de claves y despliegue detrás de proxy.
2026-09-21 — Lección 17: oráculos Petri para pruebas de pipelines C#
Sección titulada «2026-09-21 — Lección 17: oráculos Petri para pruebas de pipelines C#»- Se reutilizó el ciclo de vida ejecutable de un elemento y una plaza de la lección Petri 14 en vez de crear un segundo modelo. Su suite específica explora los ocho marcados alcanzables, clasifica los tres marcados muertos terminales y comprueba el invariante de cola Channel
free + queued = 1. - Se registró la frontera entre modelo y ejecución. Las pruebas Petri validan la especificación finita; no ejecutan
Channel<T>, TPL Dataflow ni Rx.NET y, por tanto, no demuestran la implementación actual de GA. - Cada camino del modelo se convirtió en una receta de prueba de ejecución determinista basada en compuertas en lugar de pausas. La receta exige el resultado público de excepción o finalización y el asentamiento de cada participante poseído.
- Se mantuvieron separadas las semánticas de capacidad: Channel cuenta elementos en cola en este modelo, la capacidad de un bloque de ejecución Dataflow incluye el elemento en curso y Rx no tiene límite hasta que el diseño lo añade.
- Se documentaron dos límites deliberados: la cancelación es atómica y anterior al inicio, y la red de un elemento no puede reproducir un productor ya bloqueado por backpressure tras el fallo del consumidor. Quedan por verificar un modelo de dos elementos y fixtures de ejecución.
2026-09-24 — Correcciones upstream comprobadas de nuevo
Sección titulada «2026-09-24 — Correcciones upstream comprobadas de nuevo»- Los cuatro hallazgos de las lecciones 1 a 4 se corrigieron upstream en #687, fusionada el 2026-09-20, cada uno con un test que falla con el código antiguo:
ItemsSpandevuelve su array subyacente, el-dePitchClasslee una tabla plana de 144 entradas (unos 42 ns en el benchmark de este curso, según la PR),Try.OfAsyncusaConfigureAwait(false)y deja pasar la cancelación, yLazyWithExpiration<T>comprueba una marca de tiempo en cada acceso. Comprobado en el código en GA27a1257. El curso sigue fijado aa826864, así que sus benchmarks siguen midiendo el código antiguo; no los he ejecutado contra la corrección (por verificar). - La PR señala un cambio de comportamiento:
MonadicServiceBase, en GaApi y GA.MusicTheory.Service, deja pasar ahora la cancelación de una petición en lugar de devolver unTryfallido. - Estos cuatro hallazgos solo figuraban en la entrada del 2026-09-14; ahora tienen sus filas en la tabla QA.
- #730, fusionada el 2026-09-24, corrige los hallazgos de rendimiento aún abiertos, y la exportación que no era reproducible. Medido para la PR en una máquina, en Release, 4.096 llamadas por miembro tras un calentamiento; no he vuelto a ejecutar los benchmarks de este curso contra ella (por verificar):
PitchClassSet.ClosestDiatonicKey: 308,7 ms y 175.699 bytes por llamada antes, 1,0 ms y 0 bytes después;GetCompatibleKeys: 180,5 ms y 110.116 bytes, luego 0,1 ms y 0 bytes;ToNormalFormeIsNormalFormleen una tabla de 4.096 entradas. Una prueba compara cada uno con la implementación antigua, conservada como oráculo, en los 4.096 conjuntos, desempates incluidos.ValueObjectUtils<TSelf>.Itemsdevuelve una sola colección, yValues, leído a través deIReadOnlyList<int>, ya no empaqueta 24 bytes por lectura en nueve tipos.- El camino paralelo de
VoicingGeneratoremite ahora sus ventanas en orden de trastes, así que su flujo es el secuencial. Ese orden era la causa de la diferencia entre exportaciones: la exportación conserva el primero de los voicings igual de baratos. Dos exportaciones de guitarra del main de GA tenían los hashes776c849e…yd0a7d1d9…; con la corrección,138603e8…las dos veces.
EnsureValueInRangese corrigió dentro de #598, una PR de funcionalidades, sin prueba. #731 añade una: 9 pruebas pasan en main, y 5 de ellas fallan con el archivo dea826864.
2026-10-22 — Apéndice 4: plugins sin MEF
Sección titulada «2026-10-22 — Apéndice 4: plugins sin MEF»- El autor pidió una prueba del sucesor de MEF con dos módulos, A y B, y después este apéndice, verificado aquí en lugar de devuelto. Primero hubo un prototipo fuera del repositorio; el apéndice añade los módulos C y D y se ejecuta con
check.sh. - Las hipótesis se escribieron antes de la primera ejecución de la versión del curso, y están en la tabla de experimentos. Las predicciones sobre el servicio compartido, la identidad de los tipos y el temporizador se cumplieron en la primera ejecución, con la misma salida en 31 ejecuciones en Windows 11 y el SDK de .NET 10.0.113.
- El control, los módulos A y B abandonados sin
Dispose, se añadió tras el resultado del temporizador: sin él, «el contenedor no se liberó» y «el temporizador corre» eran una sola explicación. - El módulo D pone a prueba la explicación sacada de la documentación de
Timer, según la cual un temporizador activo puede recolectarse: solo se distingue de C por un callback estático. Se descargó, pero en 40 ejecuciones del primer host, diez recolecciones seguidas, el bucle del artículo sobre la descarga, no bastaron cuatro veces: tres veces el contexto seguía vivo con su ensamblado ya fuera deGetAssemblies, una vez conModuleDtodavía en la lista. Con 10 ms entre recolecciones, tres cada vez en 40 ejecuciones. El host espera ahora hasta 100 recolecciones, con 10 ms de intervalo, e imprime el recuento en una línea#quecheck.shmuestra sin compararla. - El módulo B recibió primero Newtonsoft.Json 12.0.3; la auditoría de NuGet lo señaló (NU1903, GHSA-5crp-9r3c-p9vr), así que B usa 13.0.1. Los dos paquetes 13.x llevan la versión de ensamblado 13.0.0.0, que la salida imprime ahora.
Por verificar
Sección titulada «Por verificar»- La cadena de referencias que mantiene cargado el módulo C, volcada con el
gcrootdedotnet-dump; el apéndice 4 la deduce de dos medidas (apéndice 4). - Ejecutar las recetas de la lección 17 contra fixtures fijadas de Channel, Dataflow y Rx, incluido un ordenamiento de dos elementos con productor bloqueado; la evidencia actual es solo el oráculo Petri.
- Por qué la PGO dinámica no eliminó los enumeradores encapsulados de la primera versión con máscaras de
TryMatch(apéndice 2). - Por qué la exportación del índice OPTIC-K de GA difiere entre sesiones en 266 de las 313.047 entradas (apéndice 2).
- A qué miembro se enlaza
T.Itemscuando una interfaz derivada oculta una propiedad abstracta estática con una propiedadnew static(lección 5). - Por qué una llamada auxiliar por iteración solo hizo 1.46 veces más lento el bucle compartido de
ReadStatic<string>(lección 5). - Cuál de los dos objetos de un
foreachsobrePitchClass.Itemsdeja de asignar la PGO dinámica (lección 5). - Si
PoolingAsyncValueTaskMethodBuilderelimina los 104 bytes deValueTaskAfterYield(lección 3). - El IL de
GA.Corecompilado por Roslyn 4.11 frente al del Roslyn 5.0 del SDK. - Las lecciones se escribieron en x64; los resultados de Arm64 provienen solo de las líneas dependientes de la máquina del runner de macOS, nunca de un benchmark.