Skip to content

Commit 4b8cfcc

Browse files
santisqsdwheeler
andauthored
Use OrderedDictionary to dynamically create new objects (#11090)
* updating links to relative. fixing typo * More editorial changes --------- Co-authored-by: Sean Wheeler <[email protected]>
1 parent 6557952 commit 4b8cfcc

1 file changed

Lines changed: 176 additions & 0 deletions

File tree

reference/docs-conceptual/dev-cross-plat/performance/script-authoring-considerations.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,179 @@ Unwrapped = 42.92 ms
627627
The unwrapped example is **372 times faster**. Also, notice that the first implementation requires
628628
the **Append** parameter, which isn't required for the later implementation.
629629

630+
## Use OrderedDictionary to dynamically create new objects
631+
632+
There are situations where we may need to dynamically create objects based on some input, the
633+
perhaps most commonly used way to create a new **PSObject** and then add new properties using the
634+
`Add-Member` cmdlet. The performance cost for small collections using this technique may be
635+
negligible however it can become very noticeable for big collections. In that case, the recommended
636+
approach is to use an `[OrderedDictionary]` and then convert it to a **PSObject** using the
637+
`[pscustomobject]` type accelerator. For more information, see the _Creating ordered dictionaries_
638+
section of [about_Hash_Tables][19].
639+
640+
Assume you have the following API response stored in the variable `$json`.
641+
642+
```json
643+
{
644+
"tables": [
645+
{
646+
"name": "PrimaryResult",
647+
"columns": [
648+
{ "name": "Type", "type": "string" },
649+
{ "name": "TenantId", "type": "string" },
650+
{ "name": "count_", "type": "long" }
651+
],
652+
"rows": [
653+
[ "Usage", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
654+
[ "Usage", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
655+
[ "BillingFact", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
656+
[ "BillingFact", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
657+
[ "Operation", "63613592-b6f7-4c3d-a390-22ba13102111", "7" ],
658+
[ "Operation", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "5" ]
659+
]
660+
}
661+
]
662+
}
663+
```
664+
665+
Now, suppose you want to export this data to a CSV. First you need to create new objects and add the
666+
properties and values using the `Add-Member` cmdlet.
667+
668+
```powershell
669+
$data = $json | ConvertFrom-Json
670+
$columns = $data.tables.columns
671+
$result = foreach ($row in $data.tables.rows) {
672+
$obj = [psobject]::new()
673+
$index = 0
674+
675+
foreach ($column in $columns) {
676+
$obj | Add-Member -MemberType NoteProperty -Name $column.name -Value $row[$index++]
677+
}
678+
679+
$obj
680+
}
681+
```
682+
683+
Using an `OrderedDictionary`, the code can be translated to:
684+
685+
```powershell
686+
$data = $json | ConvertFrom-Json
687+
$columns = $data.tables.columns
688+
$result = foreach ($row in $data.tables.rows) {
689+
$obj = [ordered]@{}
690+
$index = 0
691+
692+
foreach ($column in $columns) {
693+
$obj[$column.name] = $row[$index++]
694+
}
695+
696+
[pscustomobject] $obj
697+
}
698+
```
699+
700+
In both cases the `$result` output would be same:
701+
702+
```output
703+
Type TenantId count_
704+
---- -------- ------
705+
Usage 63613592-b6f7-4c3d-a390-22ba13102111 1
706+
Usage d436f322-a9f4-4aad-9a7d-271fbf66001c 1
707+
BillingFact 63613592-b6f7-4c3d-a390-22ba13102111 1
708+
BillingFact d436f322-a9f4-4aad-9a7d-271fbf66001c 1
709+
Operation 63613592-b6f7-4c3d-a390-22ba13102111 7
710+
Operation d436f322-a9f4-4aad-9a7d-271fbf66001c 5
711+
```
712+
713+
The latter approach becomes exponentially more efficient as the number of objects and member
714+
properties increases.
715+
716+
Here is a performance comparison of three techniques for creating objects with 5 properties:
717+
718+
```powershell
719+
$tests = @{
720+
'[ordered] into [pscustomobject] cast' = {
721+
param([int] $iterations, [string[]] $props)
722+
723+
foreach ($i in 1..$iterations) {
724+
$obj = [ordered]@{}
725+
foreach ($prop in $props) {
726+
$obj[$prop] = $i
727+
}
728+
[pscustomobject] $obj
729+
}
730+
}
731+
'Add-Member' = {
732+
param([int] $iterations, [string[]] $props)
733+
734+
foreach ($i in 1..$iterations) {
735+
$obj = [psobject]::new()
736+
foreach ($prop in $props) {
737+
$obj | Add-Member -MemberType NoteProperty -Name $prop -Value $i
738+
}
739+
$obj
740+
}
741+
}
742+
'PSObject.Properties.Add' = {
743+
param([int] $iterations, [string[]] $props)
744+
745+
# this is how, behind the scenes, `Add-Member` attaches
746+
# new properties to our PSObject.
747+
# Worth having it here for performance comparison
748+
749+
foreach ($i in 1..$iterations) {
750+
$obj = [psobject]::new()
751+
foreach ($prop in $props) {
752+
$obj.PSObject.Properties.Add(
753+
[psnoteproperty]::new($prop, $i))
754+
}
755+
$obj
756+
}
757+
}
758+
}
759+
760+
$properties = 'Prop1', 'Prop2', 'Prop3', 'Prop4', 'Prop5'
761+
762+
1kb, 10kb, 100kb | ForEach-Object {
763+
$groupResult = foreach ($test in $tests.GetEnumerator()) {
764+
$ms = Measure-Command { & $test.Value -iterations $_ -props $properties }
765+
766+
[pscustomobject]@{
767+
Iterations = $_
768+
Test = $test.Key
769+
TotalMilliseconds = [math]::Round($ms.TotalMilliseconds, 2)
770+
}
771+
772+
[GC]::Collect()
773+
[GC]::WaitForPendingFinalizers()
774+
}
775+
776+
$groupResult = $groupResult | Sort-Object TotalMilliseconds
777+
$groupResult | Select-Object *, @{
778+
Name = 'RelativeSpeed'
779+
Expression = {
780+
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
781+
[math]::Round($relativeSpeed, 2).ToString() + 'x'
782+
}
783+
}
784+
}
785+
```
786+
787+
And these are the results:
788+
789+
```output
790+
Iterations Test TotalMilliseconds RelativeSpeed
791+
---------- ---- ----------------- -------------
792+
1024 [ordered] into [pscustomobject] cast 22.00 1x
793+
1024 PSObject.Properties.Add 153.17 6.96x
794+
1024 Add-Member 261.96 11.91x
795+
10240 [ordered] into [pscustomobject] cast 65.24 1x
796+
10240 PSObject.Properties.Add 1293.07 19.82x
797+
10240 Add-Member 2203.03 33.77x
798+
102400 [ordered] into [pscustomobject] cast 639.83 1x
799+
102400 PSObject.Properties.Add 13914.67 21.75x
800+
102400 Add-Member 23496.08 36.72x
801+
```
802+
630803
## Related links
631804

632805
- [`$null`][03]
@@ -643,6 +816,7 @@ the **Append** parameter, which isn't required for the later implementation.
643816
- [`[StreamReader]`][15]
644817
- [`[File]::ReadLines()` method][16]
645818
- [Write-Host][17]
819+
- [Add-Member][18]
646820

647821
<!-- Link reference definitions -->
648822
[02]: ../../learn/deep-dives/everything-about-hashtable.md
@@ -661,3 +835,5 @@ the **Append** parameter, which isn't required for the later implementation.
661835
[15]: xref:System.IO.StreamReader
662836
[16]: xref:System.IO.File.ReadLines*
663837
[17]: xref:Microsoft.PowerShell.Utility.Write-Host
838+
[18]: xref:Microsoft.PowerShell.Utility.Add-Member
839+
[19]: /powershell/module/microsoft.powershell.core/about/about_hash_tables#creating-ordered-dictionaries

0 commit comments

Comments
 (0)