¿El abastecimiento de puntos es más lento que solo leer el contenido del archivo?

13

He escrito un módulo de PowerShell que extrae definiciones de funciones de diferentes archivos fuente (es decir, un archivo .ps1 por función). Esto nos permite (como equipo) trabajar en diferentes funciones en paralelo. El módulo (archivo .psm1) obtiene la lista de archivos .ps1 disponibles ...

$Functions = Get-ChildItem -Path $FunctionPath *.ps1

... luego recorre la lista y extrae cada definición de función a través del abastecimiento de puntos:

foreach($Function in $Functions) {
  . $Function.Fullname                                     # Can be slow
}

Problema: Hemos notado que la velocidad con la que se completa esto puede variar mucho, de 10 a 180 segundos para aproximadamente 50 archivos de origen, dependiendo de la máquina que probamos. No podemos explicar la amplia variación en el tiempo necesario, y creemos que hemos controlado variables como el tipo de máquina, el sistema operativo, la cuenta de usuario, los permisos de administrador, el perfil de PS, la versión de PS, etc. El tiempo que se tarda puede variar en el mismo host para el mismo usuario de un día para otro.

Nos preguntamos si esto era un problema con el acceso al disco y probamos qué tan rápido podíamos simplemente leer desde el disco. Resulta que ejecutar Get-Contenttodos esos archivos fue muy rápido, lo que hemos aprovechado para solucionar el problema:

foreach($Function in $Functions) {
  Invoke-Expression (Get-Content $Function.Fullname -Raw)  # Is quick
}

¿Por qué agregar estas funciones a través del abastecimiento de puntos es mucho más lento que leer y ejecutar el contenido del archivo?

Charlie Joynt
fuente

Respuestas:

17

Preparando la ciencia

Primero, algunos scripts para ayudarnos a probar esto. Esto genera 2000 archivos de script, cada uno con una única función pequeña:

1..2000 | % { "Function Test$_(`$someArg) { Return `$someArg * $_ }" > "test$_.ps1" }

Eso debería ser suficiente para que la sobrecarga de inicio normal no importe demasiado. Puedes agregar más si quieres. Esto los carga a todos usando el abastecimiento de puntos:

dir test*.ps1 | % {. $_.FullName}

Esto los carga a todos leyendo primero su contenido:

dir test*.ps1 | % {iex (gc $_.FullName -Raw)}

Ahora necesitamos hacer una inspección seria de cómo funciona PowerShell. Me gusta JetBrains dotPeek para un descompilador. Si alguna vez ha intentado incrustar PowerShell en una aplicación .NET , encontrará que el ensamblado que incluye la mayoría de las cosas relevantes es System.Management.Automation. Descompilarlo en un proyecto y un PDB.

Para ver dónde se está gastando todo este tiempo misterioso, utilizaremos un generador de perfiles. Me gusta el que está integrado en Visual Studio. Es muy fácil de usar . Agregue la carpeta que contiene el PDB a las ubicaciones de los símbolos . Ahora, podemos hacer una ejecución de creación de perfiles de una instancia de PowerShell que solo ejecuta uno de los scripts de prueba. (Establezca los parámetros de la línea de comandos para usar -Filecon la ruta completa de la primera secuencia de comandos que intente. Establezca la ubicación de inicio en la carpeta que contiene todas las secuencias de comandos pequeñas.) Una vez que haya terminado, abra las Propiedades en la powershell.exeentrada en Objetivos y cambie Los argumentos para utilizar el otro script. A continuación, haga clic con el botón derecho en el elemento superior en el Explorador de rendimiento y seleccione Iniciar creación de perfiles. El generador de perfiles se ejecuta nuevamente utilizando el otro script. Ahora podemos comparar. Asegúrese de hacer clic en "Mostrar todo el código" si se le da la opción; para mí, eso aparece en un área de Notificaciones en la vista Resumen del Informe de perfil de muestra.

Los resultados llegan

En mi máquina, la Get-Contentversión tardó 9 segundos en revisar los archivos de script 2000. Las funciones importantes en el "Hot Path" fueron:

Microsoft.PowerShell.Commands.GetContentCommand.ProcessRecord
Microsoft.PowerShell.Commands.InvokeExpressionCommand.ProcessRecord

Esto tiene mucho sentido: tenemos que esperar para Get-Contentleer el contenido del disco, y tenemos que esperar para Invoke-Expressionhacer uso de esos contenidos.

En la versión de fuente de puntos, mi máquina pasó un poco más de 15 segundos para trabajar con esos archivos. Esta vez, las funciones en el Hot Path eran métodos nativos:

WinVerifyTrust
CodeAuthzFullyQualifyFilename

El segundo no parece estar documentado, pero WinVerifyTrust"realiza una acción de verificación de confianza en un objeto específico". Eso es lo más vago que puede ser, pero en otras palabras, esa función verifica la autenticidad de un recurso dado usando un proveedor dado. Tenga en cuenta que no he habilitado ningún elemento de seguridad sofisticado para PowerShell, y mi política de ejecución de script es Unrestricted.

Lo que eso significa

En resumen, está esperando que cada archivo se verifique de alguna manera, probablemente se verificó una firma, aunque eso no es necesario cuando no restringe los scripts que se pueden ejecutar. Cuando usted gcy luego iexlos contenidos, es como si hubiera escrito las funciones en la consola, por lo que no hay recursos para verificar.

Ben N
fuente
2
Ben, gracias por esta excelente respuesta. Me impresionó que fueras tan lejos como para descompilar, que es un paso más allá de todo lo que he intentado. Veré si hay alguna manera de seguir su método de prueba en una de las máquinas donde este problema es más agudo. ¡Esto podría llevar mucho tiempo, así que no contengas la respiración!
Charlie Joynt