Dónde mirar cuando un script de PowerShell es lento — claves de arrays, pipeline y cruces de datos
· Actualizado el: · Go Komura · PowerShell, Windows, Mejora de rendimiento, Automatización, Scripts, Mejora operativa, Optimización, Procesamiento de datos
«Al principio el script de agregación terminaba en pocos minutos, pero a medida que crecieron los datos pasó a tardar tres horas» — es una queja habitual en los entornos donde la automatización con PowerShell ya forma parte del día a día. Y en la mayoría de los casos, la causa de la lentitud no está en la velocidad de ejecución de PowerShell en sí, sino en la forma de escribir el código.
PowerShell es un lenguaje que prioriza la facilidad de escritura, por lo que existen varias formas de escribir código que, aun pareciendo naturales, terminan degradando la complejidad computacional. El caso más representativo es $array += $item. Se ve natural, pero internamente copia el array completo cada vez, y la lentitud se dispara a medida que aumenta el número de elementos. Conocer o no estas trampas habituales determina si el mismo proceso tarda varias horas o solo unos segundos.
En este artículo ordenamos, de mayor a menor impacto, los puntos que conviene sospechar primero al encontrarse con un script lento. Junto con eso, también explicamos la forma correcta de medir, para no reescribir el código guiándose solo por suposiciones.
El entorno de referencia es tanto Windows PowerShell 5.1 como PowerShell 7. En los puntos donde el comportamiento difiere entre ambos (por ejemplo, la codificación de caracteres predeterminada de Get-Content), lo indicamos explícitamente en el texto. El código de ejemplo del artículo se ha verificado ejecutándolo en PowerShell 7.6.
1. La conclusión primero
- No use
$array += $item. Los arrays de PowerShell tienen longitud fija, y+=crea un array nuevo y copia todos los elementos cada vez. La lentitud crece en proporción al cuadrado del número de elementos.1 - En su lugar, use
List[T]o capture en una variable la salida de todo el bucle. Esta segunda opción es a la vez idiomática de PowerShell y rápida. - Para cruces y búsquedas, convertir a tabla hash es lo más eficaz. La búsqueda lineal de un bucle anidado (O(n×m)) pasa a ser una consulta por clave (aproximadamente O(n+m)).2
- El pipeline es cómodo, pero tiene un coste por cada objeto que pasa por él. En bucles internos con un volumen grande de elementos, la instrucción
foreachsuele ser más rápida, y vale la pena medirlo.3 - La lectura de archivos se elige según el objetivo.
Get-Content -Rawpara leer todo de una vez,-ReadCountpara leer por lotes. En algunos casos, la generación de un objeto por línea es la que realmente pesa.4 - Use
-FilterconGet-ChildItem. El filtrado se aplica en el proveedor, y la propia documentación oficial indica que esto es más eficiente que descartar elementos conWhere-Objectdespués de obtenerlos.5 - Para concatenar cadenas, use
-joinoStringBuilder.$s += "..."se vuelve lento por la misma razón que los arrays.6 - Use
Format-*únicamente al final, para mostrar en pantalla. Insertarlo en medio del proceso rompe el procesamiento posterior y añade un coste de formateo innecesario.7 - Mida antes de corregir.
Measure-Commanddescarta la salida, por lo que no incluye el coste de mostrarla en pantalla. No use el valor de la primera ejecución.8 - La paralelización va al final. Considérela solo después de corregir el algoritmo (véase «Procesamiento paralelo en PowerShell»).
2. Primero, mida — el uso correcto de Measure-Command
Si se ajusta el rendimiento por suposiciones, casi siempre se termina corrigiendo un punto que no tiene efecto real. Lo primero que hay que hacer es medir.8
Antes de continuar, una aclaración. En los ejemplos de código de este artículo, Invoke-KsAggregate, ConvertTo-KsRecord y Join-KsRecord son nombres de función ficticios, pensados para que usted los sustituya mentalmente por su propio proceso. No son cmdlets estándar de PowerShell, así que si los copia y pega tal cual obtendrá el error «no se reconoce el término». Léalos reemplazándolos por los nombres de función y el procesamiento de su propio script.
# La primera ejecución incluye la carga de módulos y la compilación JIT, así que se descarta
$null = Measure-Command { Invoke-KsAggregate }
# Mida varias veces a partir de la segunda ejecución y observe también la variabilidad
1..3 | ForEach-Object {
(Measure-Command { Invoke-KsAggregate }).TotalSeconds
}
Hay dos puntos a tener en cuenta con Measure-Command.
- La salida del bloque de script se descarta. No incluye el coste de formatear y dibujar en pantalla. Si la lentitud percibida en la práctica es real, a veces el culpable está en la presentación de los datos
- Compare siempre en las mismas condiciones. El valor medido en procesos con E/S varía mucho según si la caché de archivos está «caliente» o no
Cuando quiera desglosar en qué punto exacto del proceso se pierde el tiempo, use Stopwatch.
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$rows = Import-Csv $csvPath
Write-Verbose "Lectura: $($sw.ElapsedMilliseconds) ms"; $sw.Restart()
$index = $master | Group-Object Code -AsHashTable -AsString
Write-Verbose "Creación del índice: $($sw.ElapsedMilliseconds) ms"; $sw.Restart()
$result = foreach ($r in $rows) { Join-KsRecord $r $index }
Write-Verbose "Cruce: $($sw.ElapsedMilliseconds) ms"; $sw.Stop()
Si obtiene el tiempo de cada tramo de esta manera, enseguida descubrirá hechos como que «en realidad, la lectura representaba el 80 % del tiempo total». Se usa Write-Verbose para que este detalle no aparezca en la ejecución normal y solo se vea durante la investigación (véase «Los flujos de salida y el diseño de logs en PowerShell»).
3. El principal culpable — el += en arrays
Los arrays de PowerShell tienen longitud fija. No se les pueden añadir elementos directamente: += se traduce internamente en «crear un array nuevo, copiar todos los elementos existentes y añadir el nuevo elemento al final».1 Es decir, un bucle que añade n elementos realiza en total un número de copias proporcional al cuadrado de n.
El grado de crecimiento se puede calcular directamente a partir de la forma en que está escrito el código. El número total de copias de elementos que se producen al añadir n elementos es n(n-1)/2.
| Elementos añadidos | Total de copias de elementos con += |
Número de llamadas de List[T].Add |
|---|---|---|
| 1.000 | Aprox. 500.000 | 1.000 |
| 10.000 | Aprox. 50.000.000 | 10.000 |
| 100.000 | Aprox. 5.000.000.000 | 100.000 |
Estos no son tiempos de ejecución medidos, sino el número de operaciones que queda determinado únicamente por la forma en que está escrito el código. El tiempo real por elemento varía según el entorno, así que mida los segundos en el suyo con el benchmark incluido en el zip de distribución. Sin embargo, el patrón de crecimiento en sí —que al multiplicar por diez el número de elementos, el número de operaciones se multiplica por cien— es el mismo independientemente del entorno.
# 【MAL】se vuelve drásticamente más lento a medida que aumenta el número de elementos
$result = @()
foreach ($row in $rows) {
$result += ConvertTo-KsRecord $row # se copia el array completo cada vez
}
# 【BIEN-1】usar List[T] (añadir un elemento es de tiempo constante)
$result = [System.Collections.Generic.List[object]]::new()
foreach ($row in $rows) {
$result.Add((ConvertTo-KsRecord $row))
}
# 【BIEN-2】capturar en una variable la salida de todo el bucle (idiomático en PowerShell y rápido)
$result = foreach ($row in $rows) {
ConvertTo-KsRecord $row # la salida se agrega directamente
}
Una vez que se acostumbra a ella, la forma 【BIEN-2】 es la más natural en PowerShell. La salida de foreach, ForEach-Object, if, etc., se puede capturar directamente en una variable. Esta propiedad se entiende mejor junto con el concepto de «flujos de salida».
El mismo razonamiento se aplica a las cadenas de texto. Como las cadenas son inmutables, $s += "line" crea una cadena nueva cada vez.
# 【MAL】
$text = ''
foreach ($line in $lines) { $text += "$line`r`n" }
# 【BIEN-1】-join (lo más conciso)
$text = $lines -join "`r`n"
# 【BIEN-2】StringBuilder (para construcciones complejas con ramas condicionales)
$sb = [System.Text.StringBuilder]::new()
foreach ($line in $lines) { [void]$sb.AppendLine($line) }
$text = $sb.ToString()
4. Elimine los bucles anidados — cruces con tablas hash
Lo siguiente que más impacto tiene es el cruce (matching) de datos. Si escribe de forma directa el proceso de «para cada fila de los datos de pedidos, buscar el nombre del producto en el maestro», el resultado es este:
# 【MAL】10.000 pedidos × 10.000 registros maestros = 100.000.000 de comparaciones
foreach ($order in $orders) {
$item = $master | Where-Object { $_.Code -eq $order.Code } # recorre todo el maestro cada vez
$order | Add-Member NoteProperty ItemName $item.Name
}
Si esto se convierte en una tabla hash, el número de comparaciones se reduce drásticamente.2
# 【BIEN】se crea el índice una sola vez y luego se consulta por clave
$index = $master | Group-Object -Property Code -AsHashTable -AsString
$result = foreach ($order in $orders) {
$hit = $index[$order.Code] # la consulta por clave no depende del número de elementos
[pscustomobject]@{
Code = $order.Code
Qty = $order.Qty
ItemName = if ($hit) { $hit[0].Name } else { $null } # también permite detectar registros no dados de alta
}
}
Veamos también aquí las cifras. El número de comparaciones del bucle anidado es el número de pedidos multiplicado por el número de registros maestros; con la tabla hash, en cambio, el coste es «crear el índice una vez» más «consultar elemento por elemento», por lo que es proporcional al total de elementos.
| Pedidos × registros maestros | Comparaciones del bucle anidado | Operaciones con tabla hash |
|---|---|---|
| 1.000 × 1.000 | 1.000.000 | Aprox. 2.000 |
| 10.000 × 10.000 | 100.000.000 | Aprox. 20.000 |
| 100.000 × 100.000 | 10.000.000.000 | Aprox. 200.000 |
Cuando el número de elementos se multiplica por diez, el bucle anidado se multiplica por cien y la tabla hash por diez. Cuanto mayor es el volumen, más se abre la diferencia, así que merece la pena corregir primero aquellos procesos donde se prevé que el volumen de datos crecerá.
Group-Object -AsHashTable agrupa en un array los elementos que comparten la misma clave, por lo que el valor resultante es un array (de ahí el $hit[0] del ejemplo anterior). Si sabe con certeza que las claves son únicas, también puede construir la tabla hash usted mismo.
$index = @{}
foreach ($m in $master) { $index[$m.Code] = $m } # se asume que la clave es única
Cuando lo único que se necesita comprobar repetidamente es «si está incluido o no», HashSet resulta útil. -contains y -in sobre un array realizan una búsqueda lineal, así que su coste se nota cuando el número de comprobaciones es alto.
$known = [System.Collections.Generic.HashSet[string]]::new(
[string[]]$master.Code, [System.StringComparer]::OrdinalIgnoreCase)
$unknown = $orders | Where-Object { -not $known.Contains($_.Code) }
Los patrones prácticos para cruzar archivos CSV entre sí también se tratan en «Automatizar el procesamiento de Excel y CSV con PowerShell».
5. Pipeline y la instrucción foreach
El pipeline es una funcionalidad central de PowerShell, pero tiene un coste de procesamiento por cada objeto que pasa de un cmdlet a otro. En bucles internos del orden de cientos de miles de elementos, la instrucción foreach suele ser más rápida.3
# Pipeline: fácil de leer y, al procesarse de forma secuencial, no acumula resultados intermedios
$rows | Where-Object { $_.Status -eq 'OK' } | ForEach-Object { $_.Amount } |
Measure-Object -Sum
# Instrucción foreach: la sobrecarga por elemento es menor, por lo que es más rápida con grandes volúmenes
$sum = 0
foreach ($r in $rows) { if ($r.Status -eq 'OK') { $sum += $r.Amount } }
Sobre la memoria conviene añadir una aclaración, porque es un punto donde suele haber confusión. La instrucción foreach evalúa primero la expresión entre paréntesis antes de entrar en el bucle, así que si le pasa el resultado de ejecutar un comando, como en foreach ($line in (Get-Content $path)), en ese momento se cargan en memoria todos los elementos.3 En cambio, si se le pasa un IEnumerable de enumeración diferida, los elementos se van enumerando de uno en uno. Por eso, incluso con un archivo enorme, se puede procesar sin usar memoria adicional manteniendo la instrucción foreach, si se escribe como sigue.
# procesar con foreach sin cargar todos los elementos en memoria (enumeración diferida)
foreach ($line in [System.IO.File]::ReadLines($path)) {
if ($line.StartsWith('ERROR')) { $errors++ }
}
Sin embargo, tenga en cuenta que la codificación de caracteres predeterminada es distinta. ReadLines($path) con un solo argumento lee como UTF-8 (o respeta la marca BOM si existe). En cambio, Get-Content en Windows PowerShell 5.1, cuando no hay BOM, lee usando la página de códigos ANSI actual (Shift_JIS en un entorno en japonés). Es decir, si sustituye directamente la lectura de un log en Shift_JIS, los caracteres japoneses aparecerán corruptos. Cuando haga el reemplazo, use la sobrecarga que permite indicar la codificación explícitamente.
# Caso de leer un log en Shift_JIS (CP932)
# A partir de PowerShell 6.2 el proveedor de páginas de códigos ya está registrado,
# así que se puede llamar directamente a GetEncoding(932) (lo mismo en Windows PowerShell 5.1)
$enc = [System.Text.Encoding]::GetEncoding(932)
foreach ($line in [System.IO.File]::ReadLines($path, $enc)) {
if ($line.StartsWith('ERROR')) { $errors++ }
}
Además, como en PowerShell 7 la codificación predeterminada de Get-Content es UTF-8 sin BOM, mientras trabaje con archivos UTF-8 en PowerShell 7 los resultados coincidirán incluso con la sobrecarga de un solo argumento. Antes de hacer el reemplazo, verifique siempre la codificación del archivo de destino y la versión del entorno de ejecución.
A modo de guía orientativa:
| Situación | Elección |
|---|---|
| Pocos elementos, se prioriza la legibilidad | Pipeline |
Entrada enorme pasada como resultado de un comando (por ejemplo, Get-Content) |
Pipeline, o bien [IO.File]::ReadLines + instrucción foreach |
| Recorrer cientos de miles de elementos de un array que ya está en memoria | Instrucción foreach |
| Filtrado simple sobre un array | Métodos .Where({...}) / .ForEach({...})1 |
.Where() y .ForEach() son métodos de array añadidos en PowerShell 4.0, y al no pasar por el pipeline resultan más ligeros.1 No obstante, se parte de la base de que el objeto ya es una colección en memoria.
6. E/S de archivos y directorios
La lectura se elige según el objetivo. Get-Content, por defecto, genera un objeto por cada línea, así que en archivos enormes este coste termina dominando el tiempo total.4
Get-Content $path # línea por línea (genera un objeto por cada línea)
Get-Content $path -Raw # lee todo el archivo de una vez como una única cadena
Get-Content $path -ReadCount 1000 # entrega arrays de 1000 líneas cada vez (reduce la generación de objetos)
[System.IO.File]::ReadLines($path) # .NET. Enumeración secuencial, la más ligera (UTF-8 por defecto)
Para recorrer directorios, use -Filter. La documentación oficial indica explícitamente que «el filtro lo aplica el proveedor en el momento de obtener los objetos, por lo que es más eficiente que otros parámetros».5
# 【MAL】obtiene todos los archivos y luego los descarta
Get-ChildItem -Path $root -Recurse | Where-Object { $_.Extension -eq '.log' }
# 【BIEN】filtra en el propio proveedor
Get-ChildItem -Path $root -Recurse -File -Filter '*.log'
# Con cientos de miles de archivos, si sigue siendo lento, considere la enumeración de .NET
[System.IO.Directory]::EnumerateFiles($root, '*.log', 'AllDirectories')
En la escritura, el cuello de botella típico es ir añadiendo contenido dentro de un bucle en cada iteración. Add-Content abre y cierra el archivo en cada llamada.
# 【MAL】10.000 líneas = 10.000 aperturas/cierres
foreach ($r in $result) { Add-Content -Path $out -Value ($r -join ',') }
# 【BIEN-1】escribir todo de una sola vez
$result | ForEach-Object { $_ -join ',' } | Set-Content -Path $out -Encoding utf8
# 【BIEN-2】si necesita escritura secuencial, mantenga abierto un StreamWriter
$writer = [System.IO.StreamWriter]::new($out, $false, [System.Text.UTF8Encoding]::new($false))
try { foreach ($r in $result) { $writer.WriteLine($r -join ',') } }
finally { $writer.Dispose() }
En las operaciones de archivo a través de la red, lo que termina dominando es el propio número de idas y vueltas. Para las particularidades de las rutas UNC, consulte «Trampas de los recursos compartidos de red y las rutas UNC».
7. Costes pequeños que suelen pasarse por alto
Para cuando no sepa por dónde empezar, incluimos una estimación de la magnitud del efecto de cada uno. No son valores medidos, sino una idea del orden de magnitud que resulta de «coste por ejecución × número de ejecuciones». Interprételo aplicándolo al volumen de su propio script.
- La frecuencia de actualización de
Write-Progress. Si actualiza el progreso en cada iteración del bucle, el coste de dibujarlo puede llegar a superar el tiempo de procesamiento real. Reduzca la frecuencia (por ejemplo, cada 100 elementos) o desactívelo con$ProgressPreference = 'SilentlyContinue'en ejecuciones desatendidas ── impacto estimado: alto. El dibujo por iteración es pesado y el número de vueltas del bucle influye directamente. Con bucles de decenas de miles de iteraciones o más, revíselo en primer lugar - Insertar
Format-Table/Format-Listen medio del proceso. El objeto se convierte a un tipo de formato para presentación, lo que rompe el procesamiento posterior y añade un coste innecesario. Reserve la presentación para el final7 ── impacto estimado: medio. El daño real es mayor por «romper el procesamiento posterior» que por la velocidad en sí, así que vale la pena corregirlo aunque no sea por rendimiento - Procesos invariables dentro del bucle. Sacar fuera del bucle cosas como la generación de la cadena de formato de
Get-Date, la compilación de expresiones regulares o la recarga de módulos puede tener un efecto notable ── impacto estimado: medio a alto. Depende del coste de cada proceso extraído; si se mezcla algo pesado, como la recarga de un módulo, el efecto puede ser drástico - Uso excesivo de
Select-Object -Property. Genera un nuevo PSCustomObject, algo que no se puede ignorar con volúmenes grandes. A veces es más rápido conservar el objeto original hasta la etapa en que realmente se necesite ── impacto estimado: bajo a medio. No se nota con unos pocos miles de elementos, pero empieza a pesar a partir de decenas de miles - Excepciones frecuentes. Un diseño que pasa constantemente por
try/catch(por ejemplo, ejecutarGet-Itemsobre un archivo inexistente cada vez y capturar la excepción) resulta muy costoso. Ramifique de antemano conTest-Path── impacto estimado: alto. Lanzar y capturar una excepción es mucho más pesado que una ramificación normal, así que el coste domina cuanto mayor es el producto de número de elementos por tasa de excepciones
8. Reglas prácticas (tabla de decisión)
| Síntoma | Punto a sospechar | Solución | Más detalles |
|---|---|---|---|
| Se vuelve lento de golpe al aumentar el número de elementos | $array += / $string += |
List[T], capturar la salida del bucle en una variable, -join1 |
Sección 3 |
| El cruce entre dos conjuntos de datos no termina | Búsqueda lineal en bucle anidado | Tabla hash / Group-Object -AsHashTable2 |
Sección 4 |
| La lectura de un archivo enorme es lenta | Generación de un objeto por línea en Get-Content |
-Raw / -ReadCount / [File]::ReadLines4 |
Sección 6 (la advertencia sobre codificación está en la sección 5) |
| El recorrido de carpetas es lento | Filtrado posterior con Where-Object |
Get-ChildItem -Filter, y si hace falta, EnumerateFiles5 |
Sección 6 |
| La escritura del archivo de salida es lenta | Add-Content dentro de un bucle |
Escritura en bloque, o StreamWriter6 |
Sección 6 |
| La CPU está ociosa pero el proceso no termina | Espera de red o de E/S | Aquí entra la paralelización | Otro artículo: «Procesamiento paralelo» |
| La presentación en pantalla es lenta | Procesamiento de formato y dibujado | Format-* solo al final, desactivar $ProgressPreference7 |
Sección 7 |
| No se sabe dónde está la lentitud | Falta de medición | Medición por tramos con Stopwatch, Measure-Command a partir de la segunda ejecución8 |
Sección 2 |
9. Resumen
- Mida primero. Tenga en cuenta que
Measure-Commanddescarta la salida y no incluye el coste de presentación, y que el valor de la primera ejecución no es utilizable. $array +=se vuelve lento en proporción al cuadrado del número de elementos. Sustituirlo porList[T]o por capturar la salida del bucle en una variable es la máxima prioridad.- Para los cruces, convertir a tabla hash es la mejora con mejor relación coste-beneficio. Si encuentra un bucle anidado, plantéese si puede crear un índice.
- Elija la lectura y escritura de archivos según el objetivo. Lo básico es filtrar en el proveedor con
-Filter, escribir en bloque y usarStreamWritercuando corresponda. - No insertar
Format-*en medio del proceso, reducir la frecuencia de actualización del progreso, sacar del bucle los procesos invariables ── estas pequeñas acumulaciones también pesan cuando el volumen es grande. - La paralelización es el último recurso. Aplíquela solo después de corregir el algoritmo, y a procesos donde predomine el tiempo de espera.
Descarga del código de ejemplo
El código tratado en este artículo se distribuye en un paquete listo para ejecutar. Incluye tres tipos de benchmarks y un asistente de medición.
Descargar el código de ejemplo (zip)
Los ejemplos de este artículo se han verificado ejecutándolos realmente en PowerShell 7.6 (8 pruebas de Pester). Ejecutando Invoke-SampleTests.ps1, incluido en el zip, puede reproducir la misma verificación en su propio equipo.
# Análisis sintáctico + análisis estático + pruebas de Pester
./Invoke-SampleTests.ps1
Los valores de configuración (rutas, nombres de servidor, ID de inquilino, etc.) son ejemplos. No los ejecute tal cual en un entorno de producción: adáptelos al entorno de su propia empresa.
Artículos relacionados
- Procesamiento paralelo en PowerShell — cuándo usar ForEach-Object -Parallel y cuándo usar trabajos (jobs)
- Deje de usar Write-Host — los flujos de salida y el diseño de logs en PowerShell
- Automatizar el procesamiento de Excel y CSV con PowerShell — recetas prácticas de agregación, cruce y generación de informes
- Colección de comandos prácticos de PowerShell — amplíe su repertorio de pequeñas funciones de uso diario
- Cómo comparar correctamente la velocidad de distintas versiones de un programa en Windows
- Identificar la causa de la lentitud con PerfView y dotnet-trace
Áreas de consultoría relacionadas
En KomuraSoft LLC nos encargamos de acelerar procesos de agregación y cruce que tardan demasiado, de rediseñar el procesamiento para que resista el aumento del volumen de datos, y de investigar las causas de la degradación del rendimiento.
- Consultoría técnica y revisión de diseño
- Investigación de fallos y análisis de causas
- Migración y aprovechamiento de activos existentes
- Contacto
Referencias
-
Microsoft Learn, about_Arrays. Sobre el hecho de que los arrays de PowerShell tienen un tamaño fijo y de que el operador += crea un array nuevo, copia los elementos existentes y añade el nuevo elemento; sobre la conveniencia de usar un tipo de colección (como List) cuando se repiten operaciones de añadir y quitar elementos; y sobre los métodos .Where() y .ForEach(), añadidos en PowerShell 4.0. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Group-Object. Sobre cómo -AsHashTable permite devolver el resultado de la agrupación como una tabla hash, cómo -AsString permite tratar la clave como una cadena, y sobre el hecho de que el valor de la tabla hash devuelta es un array con los elementos de cada grupo. ↩ ↩2 ↩3
-
Microsoft Learn, about_Foreach. Sobre el hecho de que la instrucción foreach evalúa la expresión entre paréntesis antes de comenzar la iteración —por lo que, si se le pasa el resultado de ejecutar un comando, ese resultado se retiene en memoria—, frente al cmdlet ForEach-Object, que recibe la entrada de forma secuencial desde el pipeline; y sobre cuándo usar cada uno. Como ejemplo de enumeración diferida, el método File.ReadLines explica que permite enumerar línea por línea sin cargar el archivo completo (a diferencia de ReadAllLines). ↩ ↩2 ↩3
-
Microsoft Learn, Get-Content. Sobre el hecho de que, por defecto, devuelve un objeto por cada línea separada por saltos de línea; que -Raw permite leer el archivo completo como una única cadena; y que -ReadCount permite enviar al pipeline bloques de un número determinado de líneas a la vez. ↩ ↩2 ↩3
-
Microsoft Learn, Get-ChildItem. Sobre el hecho de que -Filter lo aplica el proveedor en el momento de obtener los objetos, por lo que es más eficiente que otros parámetros que filtran después de la obtención; y sobre su combinación con -File y -Recurse. ↩ ↩2 ↩3
-
Microsoft Learn, clase StringBuilder. Sobre el hecho de que, como String es inmutable, cada concatenación genera una nueva instancia, frente a StringBuilder, que construye la cadena sobre un búfer mutable. Junto con ello, sobre el comportamiento de la clase StreamWriter al escribir de forma secuencial manteniendo el flujo abierto. ↩ ↩2
-
Microsoft Learn, Format-Table. Sobre el hecho de que los cmdlets de la familia Format generan un objeto de formato para presentación, por lo que no son adecuados para canalizarlos a otro comando y deben usarse al final del pipeline. ↩ ↩2 ↩3
-
Microsoft Learn, Measure-Command. Sobre el hecho de que mide el tiempo de ejecución de un bloque de script o comando y devuelve un TimeSpan, sin incluir en el resultado la salida propia del objeto medido. Junto con ello, sobre la medición por tramos mediante la clase Stopwatch. ↩ ↩2 ↩3
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Deje de usar Write-Host — Flujos de salida de PowerShell y diseño de registros
Explica los seis flujos de salida de PowerShell, los problemas de Write-Host y su uso correcto, por qué se contamina el valor de retorno ...
Procesamiento paralelo en PowerShell — Cuándo usar ForEach-Object -Parallel y cuándo usar jobs
Diferencias entre ForEach-Object -Parallel, Start-ThreadJob y Start-Job, uso de $using:, ThrottleLimit y cuándo el paralelismo resulta má...
Cómo llamar correctamente a un exe externo desde PowerShell — la trampa de las comillas en argumentos, los códigos de salida y la codificación de caracteres
Al llamar a robocopy o a un EXE interno desde PowerShell, los argumentos se corrompen, no se obtiene el código de salida o la salida se v...
El manejo seguro de credenciales en PowerShell — Cómo desterrar las contraseñas en texto plano de los scripts
Organiza el procedimiento para migrar las contraseñas en texto plano de scripts de PowerShell a un almacenamiento seguro: SecureString, D...
Diseño de parámetros y modularización de scripts de PowerShell — de un «script que funciona» a un «script que se puede entregar»
Explicamos cómo llevar un script de PowerShell a una calidad entregable: param, [CmdletBinding()], validación, pipeline, -WhatIf, módulos...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Por qué es lenta la forma de escribir $array += $item?
- Porque los arrays de PowerShell tienen longitud fija y no se les pueden añadir elementos directamente. El operador += se traduce en el proceso de 'crear un array nuevo, copiar todos los elementos existentes y añadir el nuevo al final'. Como se copian todos los elementos cada vez que se añade uno, al añadir n elementos la complejidad computacional resulta proporcional al cuadrado de n. Con 100 elementos no se nota, pero al superar los 10.000 la lentitud se vuelve perceptible, y con 100.000 deja de ser viable en la práctica. La solución es usar System.Collections.Generic.List[T] y llamar a Add, o bien capturar en una variable la salida de todo el bucle.
- He medido con Measure-Command, pero no coincide con el tiempo de ejecución real.
- Esto ocurre porque Measure-Command solo mide el tiempo de ejecución del bloque de script, y su salida se descarta. En la ejecución real se suman el coste de formatear el resultado para mostrarlo en pantalla (el procesamiento de formato) y el tiempo de dibujado en la consola. Además, la primera ejecución incluye el tiempo de carga de módulos y de compilación JIT, por lo que en la mayoría de los casos el valor de la primera medición no es fiable. Respete estos dos puntos: ejecute el mismo proceso varias veces y observe los valores a partir de la segunda ejecución, y mida siempre los elementos que compara en las mismas condiciones.
- El cruce entre dos archivos CSV no termina. ¿Qué debería corregir?
- Es muy probable que en el bucle interno se esté realizando una búsqueda lineal con Where-Object en cada iteración. Si cada conjunto tiene 10.000 elementos, el número de comparaciones asciende a 100 millones. Si convierte uno de los dos conjuntos en una tabla hash (o usa Group-Object -AsHashTable) y consulta por clave, el número de comparaciones se reduce hasta ser aproximadamente proporcional al total de elementos. Es la mejora con mejor relación coste-beneficio, y su efecto es mayor cuanto más crece el volumen de datos.
- He oído que llamar directamente a las API de .NET es más rápido que usar los cmdlets. ¿Siempre debería hacerlo así?
- No, depende del caso. Las API de .NET (por ejemplo, ReadLines o EnumerateFiles de System.IO.File) son ciertamente rápidas, pero se pierde la comodidad de las funciones de proveedor de PowerShell, los comodines y la resolución de rutas relativas, y el código se vuelve más difícil de leer. Primero mida, y sustituya únicamente los puntos que resulten ser realmente el cuello de botella. Lo más práctico es limitarlo a situaciones donde el efecto es claro, como recorrer cientos de miles de archivos o leer archivos enormes.
- ¿El proceso se vuelve más rápido si lo paralelizo?
- Es efectivo cuando predomina el tiempo de espera, pero por orden de prioridad va al final. Primero reduzca el procesamiento innecesario (limitar el número de elementos obtenidos, sacar del bucle los procesos invariables, convertir los cruces a tabla hash). Si el algoritmo sigue siendo O(n²), paralelizarlo solo lo acelera en proporción al número de núcleos. Al contrario, si paraliza un proceso donde cada elemento es liviano, la sobrecarga puede incluso hacerlo más lento.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.