MSXFAQ MeetNow aktiv: Komm doch einfach dazu.

PowerShell Variablen Speicherbedarf

Ich schaue ja immer mal gerne etwas hinter die Kulissen und möchte z.B. den Speicherbedarf von Variablen abschätzen. Kann ich von einem Dateiserver alle Dateien und Verzeichnisse mal eben mit Ger-ChildItem in eine Variable laden oder sollte ich besser schon während des Einlesens die Daten verarbeiten oder sogar eine richtige Datenbank bemühen. Hier mein unvollständiger Versuch einer Analyse.

Größe ermitteln?

Leider kann man in Powershell nicht einfach den Speicherbedarf einer Variablen direkt ermitteln. Als Muster habe ich alle ca. 244537 Dateien in C:\Windows eingelesen:

Code Eignung Ausgabe

SizeOf-Propery

Für einfache Variablen gibt es eine passende Funktion im -NET-Framework

[System.Runtime.InteropServices.Marshal]::SizeOf([int])
[System.Runtime.InteropServices.Marshal]::SizeOf([string])

Wenn ich Copilot und anderen KIs glaube, sollte es gehen. Bei mir kommt aber immer nur ein Fehler:

"MethodInvocationException: Exception calling "SizeOf" with "1" argument(s): "Type 'System.RuntimeType' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.""

Interessanterweise hat mit Copilot sogar einen Code geschrieben, der auch komplexe Elemente abarbeitet.

Das sieht erst einmal professionell aus aber die Ergebnisse sind komplett daneben. Ein Array mit 10.000 unterschiedlichen Strings wurde mit 548 Byte angegeben.

Nein 

Keine

Workingset des Prozess

Speicher des Prozess vor und nach der Belegung der Variable ermitteln.

$vorher= (Get-Process -Id $pid).WorkingSet
$var = Get-ChildItem C:\Windows -recurse
Write-Host "WorkingSet Zunahme $((Get-Process -Id $pid).WorkingSet - $vorher)"

Der Wert erscheint mir etwas hoch. Und wenn eine Variable wieder frei wird, muss eigentlich noch der "Garbage Collector" laufen


Sehr grob 

710.496.256

Abfrage des GC

Aber das kann man ja steuern, indem ich erst einen Cleanup starte, dann die aktuelle Belegung merke und nach der Zuweisung der Variable wieder abfrage.

[GC]::Collect()
$vorher = [GC]::GetTotalMemory($true)

$var = Get-ChildItem C:\Windows -recurse
[GC]::Collect()
Write-Host "Bytes used: $([GC]::GetTotalMemory($true) - $vorher)"

Das wären dann ca. 22 Bytes pro Verzeichniseintrag. Das kann ich kaum glauben, denn die Summe aller Pfade durch die Anzahl ergibt schon eine durchschnittliche Pfadlänge von 117 Zeichen. Laut KI soll das aber der beste und genaueste Weg sein. Vielleicht komprimiert .NET ja die Inhalte, um Platz zu sparen


zu nierdig

 5.578.448 

Variable in Byte konvertieren

Variable als Bytes ausgeben und aufaddieren.

$var = Get-ChildItem C:\Windows -recurse
$size = [System.Text.Encoding]::Unicode.GetBytes($var)
Write-Host "Stringauflistung $($size)"

Allerdings funktioniert dies nicht bei allen Typen. Eine Hashtable mit 10.000 Einträge liefert nur 56 Bytes.


Nur wenige Basistypen 

594.06.942

Größe über Stream ermitteln

Variable in einen Stream umschreiben und Speichergröße berechnen (in Byte)

Der Code ist nicht mehr nutzbar, das Serialisierung/Deserialisierung mit Net 9.0 entfernt wurde. Siehe auch
Deserialization risks in use of BinaryFormatter and related types
https://learn.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide

$var = Get-ChildItem C:\Windows -recurse
$stream = New-Object System.IO.MemoryStream
$formatter = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
$formatter.Serialize($stream, $var)
$byteSize = $stream.Length
$stream.Close()

Nicht mehr verfügbar (Sicherheitsrisiken) 

-

Auf der einen Seite kann ich nicht glauben, dass ein einfacher "Dir /s" über das Windows Systemverzeichnis mit ca. 250.000 Verzeichnissen und Dateien fast 600 MB RAM benötigt. Das über 2,5kByte/Objekt.

Eine Information in verschiedenen Variablen

Achtung:
Wenn Sie eine Variable immer weiter "erweitern", dann kann das je nach Variable sehr ineffektiv sein, weil die PowerShell manchmal die Daten erst in einen größeren Bereich kopiert um diese dann zu erweitern.

Exemplarisch habe ich 10.000 mal einen String mit 10 Zeichen kopiert. ich hätte im besten Fall etwas mit 100.000 Bytes, Bei Unicode wegen mir auch 200.000 Bytes erwartet

Code Status Bytes

Hashtable

$var=@{}
0..9999 | %{$var += "1234567890"}
$var

 

56

Es kommt zwar ein numerischer Wert heraus, aber der passtdefinitiv nicht.

dynamisches Array

$var=@()
0..9999 | %{$var+=("1234567890$_")}
[System.Text.Encoding]::Unicode.GetBytes($var).count

 

Leer: 0
Gefüllt: 297786

Das könnte passen

Statische Array

$var = [string[]]::new(10000)
0..9999 | %{$var[$_]="1234567890$_"}

 

Leer: 19998
Gefüllt: 297778

Das könnte passen

Stack

$DirectoryStack = [System.Collections.Generic.Stack[string]]::new()
$DirectoryStack.push("1234567890$_"}

 

 

Leer: "nichts"
Gefüllt: "GetBytes" funktioniert nicht

Ein Fehler verhindert die Ermittlung einer Größe

Genau genommen habe ich auch so keinen zuverlässigen Indikator, um die Größe einer Variable im Speicher zu ermitteln. Und hier habe hier noch nicht einmal die Inhalte unterschieden.

String oder Objekt?

Das mache ich einmal für das Beispiel mit den Verzeichnissen. Wen ich mit Get-Item/Get-Childitem ein Verzeichnis bekomme, dann kann ich das einfach in ein Array ablegen. Dann habe ich aber das komplette Objekt. Wenn ich aber nur den Verzeichnisnamen als Pfad für eine spätere Weiterverarbeitung benötige, dann sollte ich vielleicht auch nur diese Information als String speichern. Gleiche Überlegungen können Sie mit "Get-ADUser" anstellen. Wollen und müssen Sie das komplette LDAP-Objekt speichern oder reicht eine Teilmenge oder einfach nur der Distinguishedname?

Code Eignung Ergebnis

String in Array

Zuerst habe ich ein Array definiert und den Fullname aller 250.000 Verzeichniseinträge gespeichert

$var=@()
get-childitem c:\windows -Recurse | foreach {$var+= $_.fullname}
$var.count
[System.Text.Encoding]::Unicode.GetBytes($var).count

Für einfache Typen
59.407.074

59 Megabyte könnte hinkommen.

Verzeichnisobject in Array

$var=@()
get-childitem c:\windows -Recurse | foreach {$var+= $_}
[System.Text.Encoding]::Unicode.GetBytes($var).count

Nicht plausibel 
59.407.074

Das kann nun aber wieder nicht stimmen. Warum sollte das Objekt mit viel mehr Informationen genauso viel Platz verbrauchen wie ein String.

Ich wollte eigentlich damit zeigen, dass die Ablage eines "Strings" in einer Variable viel Platzsparender im Vergleich zum kompletten Objekt ist. Genau das kann ich mit "GetBytes()" nicht zeigen.

Zwischenstand

Damit bin ich nach all den Checks und Tests nicht wirklich schlauer. Zuerst hatte ich gedacht, dass ein "GetBytes()" mit eine halbwegs passende Schätzung liefert aber die Methode gibt es nicht bei allen Klassen und bei einem einfachen Vergleich der Speicherung eines Objects oder Strings zeigt, dass die Methode nicht passt. Sicher könnte ich die Variable nun mit "ExportCliXML" in eine XML-Struktur konvertieren und damit die Größe abschätzen aber das ist nur ein vager Hinweis, wie groß der Platzbedarf im Hauptspeicher ist. Gerade eine Hashtable ist hier ja ein gutes Beispiel, da neben dem Key und dem Value auch ein Hash zum Key zusätzlich gespeichert wird, um schnell zugreifen zu können.

Auf die Frage der Geschwindigkeit bin ich noch gar nicht eingegangen. Dauert das Einspeichern eines String in einem Array länger oder Kürzer als die Ablage auf einem Stack oder in einer Hashtabelle?

Ich habe an der Stelle nun erst einmal abgebrochen. Ich habe keinen zuverlässigen Weg gefunden, den Speicherbedarf zuverlässig abzufragen. Vielleicht ist es eine Lösung im Skript selbst zu prüfen, wie stark man das System belastet und notfalls abzubrechen. Oder sie lassen es darauf ankommen, wie Windows Speicher ins Pagefile verschiebt.
Wenn Sie eine bessere Version kennen, dann lassen Sie mich das gerne wissen.

Weitere Links