Skip to content

Commit e6a15f9

Browse files
authored
tftypes: Reduce AttributePath and Value type compute and memory usage (#308)
Reference: #307 These optimizations were performed by adding benchmark tests against a set of 1000 "simple" objects and viewing the cpu/memory profiles. The original implementations were spending an immense amount of time dealing with memory allocations and garbage collection, so reducing memory allocations was the main target of these optimizations, which in turn, also reduced compute time. The largest changes were accomplished by removing `(Value).Diff()` from the logic paths which were only testing equality. The new `(Value).deepEqual()` implementation started as a duplicate of that logic, removing all `ValueDiff` allocations. Then, the walking of `Value` needed a methodology to stop the walk immediately to prevent further allocations. The two changes in that regard were introducing a `stopWalkError` sentinel error type for which callback functions can signal that no remaining `Value` are necessary to traverse and updating the internal `walk()` implementation to include a new bool return to fully accomplish the intended behavior. The other changes were smaller optimizations noticed from the profiling, such as avoiding the Go runtime immediately reallocating a larger slice with the `AttributePath` methods, avoiding memory allocations caused by `(Type).Is()` usage instead of using type switches, and directly referencing `List`, `Map`, `Object`, `Set`, and `Tuple` value storage instead of allocating an unneeded variable. This logic is heavily used both by this Go module and others, so even these minor optimizations have impact at scale. `benchstat` differences between prior implementation and proposed changes: ``` goos: darwin goarch: arm64 pkg: github.com/hashicorp/terraform-plugin-go/tftypes │ original │ proposed │ │ sec/op │ sec/op vs base │ Transform1000-10 2886.1µ ± 0% 924.6µ ± 0% -67.96% (p=0.000 n=10) ValueApplyTerraform5AttributePathStep1000-10 3863.4µ ± 1% 628.9µ ± 0% -83.72% (p=0.000 n=10) geomean 3.339m 762.5µ -77.16% │ original │ proposed │ │ B/op │ B/op vs base │ Transform1000-10 3137.3Ki ± 0% 837.6Ki ± 0% -73.30% (p=0.000 n=10) ValueApplyTerraform5AttributePathStep1000-10 3887.1Ki ± 0% 389.3Ki ± 0% -89.98% (p=0.000 n=10) geomean 3.410Mi 571.0Ki -83.65% │ original │ proposed │ │ allocs/op │ allocs/op vs base │ Transform1000-10 61.07k ± 0% 14.02k ± 0% -77.05% (p=0.000 n=10) ValueApplyTerraform5AttributePathStep1000-10 123.07k ± 0% 17.64k ± 0% -85.67% (p=0.000 n=10) geomean 86.69k 15.72k -81.86% ```
1 parent 653d9f3 commit e6a15f9

12 files changed

+1006
-182
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: BUG FIXES
2+
body: 'tftypes: Significantly reduced compute and memory usage of `Value` type
3+
walking and transformation'
4+
time: 2023-06-30T10:36:55.119505-04:00
5+
custom:
6+
Issue: "307"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: BUG FIXES
2+
body: 'tftypes: Removed unnecessary memory allocations from `AttributePath` type
3+
`Equal()`, `LastStep()`, and `WithoutLastStep()` methods'
4+
time: 2023-06-30T14:47:01.127283-04:00
5+
custom:
6+
Issue: "307"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: 'tftypes: Removed unnecessary memory allocations from `NewValue()` function'
3+
time: 2023-06-30T14:49:45.828209-04:00
4+
custom:
5+
Issue: "307"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: 'tftypes: Added `AttributePath` type `NextStep()` method, which returns the
3+
next step in the path without first copying via `Steps()`'
4+
time: 2023-06-30T14:38:17.600608-04:00
5+
custom:
6+
Issue: "307"

tftypes/attribute_path.go

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,20 @@ func (a *AttributePath) String() string {
8080
// AttributePaths are considered equal if they have the same number of steps,
8181
// the steps are all the same types, and the steps have all the same values.
8282
func (a *AttributePath) Equal(o *AttributePath) bool {
83-
if len(a.Steps()) == 0 && len(o.Steps()) == 0 {
84-
return true
83+
if a == nil {
84+
return o == nil || len(o.steps) == 0
85+
}
86+
87+
if o == nil {
88+
return len(a.steps) == 0
8589
}
86-
if len(a.Steps()) != len(o.Steps()) {
90+
91+
if len(a.steps) != len(o.steps) {
8792
return false
8893
}
89-
for pos, aStep := range a.Steps() {
90-
oStep := o.Steps()[pos]
94+
95+
for pos, aStep := range a.steps {
96+
oStep := o.steps[pos]
9197

9298
if !aStep.Equal(oStep) {
9399
return false
@@ -120,63 +126,112 @@ func (a *AttributePath) NewError(err error) error {
120126
}
121127
}
122128

123-
// LastStep returns the last step in the path. If the path was
124-
// empty, nil is returned.
129+
// LastStep returns the last step in the path. If the path is nil or empty, nil
130+
// is returned.
125131
func (a *AttributePath) LastStep() AttributePathStep {
126-
steps := a.Steps()
132+
if a == nil || len(a.steps) == 0 {
133+
return nil
134+
}
127135

128-
if len(steps) == 0 {
136+
return a.steps[len(a.steps)-1]
137+
}
138+
139+
// NextStep returns the next step in the path. If the path is nil or empty, nil
140+
// is returned.
141+
func (a *AttributePath) NextStep() AttributePathStep {
142+
if a == nil || len(a.steps) == 0 {
129143
return nil
130144
}
131145

132-
return steps[len(steps)-1]
146+
return a.steps[0]
133147
}
134148

135149
// WithAttributeName adds an AttributeName step to `a`, using `name` as the
136150
// attribute's name. `a` is copied, not modified.
137151
func (a *AttributePath) WithAttributeName(name string) *AttributePath {
138-
steps := a.Steps()
152+
if a == nil {
153+
return &AttributePath{
154+
steps: []AttributePathStep{AttributeName(name)},
155+
}
156+
}
157+
158+
// Avoid re-allocating larger slice
159+
steps := make([]AttributePathStep, len(a.steps)+1)
160+
copy(steps, a.steps)
161+
steps[len(steps)-1] = AttributeName(name)
162+
139163
return &AttributePath{
140-
steps: append(steps, AttributeName(name)),
164+
steps: steps,
141165
}
142166
}
143167

144168
// WithElementKeyString adds an ElementKeyString step to `a`, using `key` as
145169
// the element's key. `a` is copied, not modified.
146170
func (a *AttributePath) WithElementKeyString(key string) *AttributePath {
147-
steps := a.Steps()
171+
if a == nil {
172+
return &AttributePath{
173+
steps: []AttributePathStep{ElementKeyString(key)},
174+
}
175+
}
176+
177+
// Avoid re-allocating larger slice
178+
steps := make([]AttributePathStep, len(a.steps)+1)
179+
copy(steps, a.steps)
180+
steps[len(steps)-1] = ElementKeyString(key)
181+
148182
return &AttributePath{
149-
steps: append(steps, ElementKeyString(key)),
183+
steps: steps,
150184
}
151185
}
152186

153187
// WithElementKeyInt adds an ElementKeyInt step to `a`, using `key` as the
154188
// element's key. `a` is copied, not modified.
155189
func (a *AttributePath) WithElementKeyInt(key int) *AttributePath {
156-
steps := a.Steps()
190+
if a == nil {
191+
return &AttributePath{
192+
steps: []AttributePathStep{ElementKeyInt(key)},
193+
}
194+
}
195+
196+
// Avoid re-allocating larger slice
197+
steps := make([]AttributePathStep, len(a.steps)+1)
198+
copy(steps, a.steps)
199+
steps[len(steps)-1] = ElementKeyInt(key)
200+
157201
return &AttributePath{
158-
steps: append(steps, ElementKeyInt(key)),
202+
steps: steps,
159203
}
160204
}
161205

162206
// WithElementKeyValue adds an ElementKeyValue to `a`, using `key` as the
163207
// element's key. `a` is copied, not modified.
164208
func (a *AttributePath) WithElementKeyValue(key Value) *AttributePath {
165-
steps := a.Steps()
209+
if a == nil {
210+
return &AttributePath{
211+
steps: []AttributePathStep{ElementKeyValue(key)},
212+
}
213+
}
214+
215+
// Avoid re-allocating larger slice
216+
steps := make([]AttributePathStep, len(a.steps)+1)
217+
copy(steps, a.steps)
218+
steps[len(steps)-1] = ElementKeyValue(key)
219+
166220
return &AttributePath{
167-
steps: append(steps, ElementKeyValue(key.Copy())),
221+
steps: steps,
168222
}
169223
}
170224

171225
// WithoutLastStep removes the last step, whatever kind of step it was, from
172226
// `a`. `a` is copied, not modified.
173227
func (a *AttributePath) WithoutLastStep() *AttributePath {
174-
steps := a.Steps()
175-
if len(steps) == 0 {
228+
if a == nil || len(a.steps) == 0 {
176229
return nil
177230
}
231+
178232
return &AttributePath{
179-
steps: steps[:len(steps)-1],
233+
// Paths are immutable, so this should be safe without copying.
234+
steps: a.steps[:len(a.steps)-1],
180235
}
181236
}
182237

@@ -296,7 +351,7 @@ type AttributePathStepper interface {
296351
// types need to use the AttributePathStepper interface to tell
297352
// WalkAttributePath how to traverse themselves.
298353
func WalkAttributePath(in interface{}, path *AttributePath) (interface{}, *AttributePath, error) {
299-
if len(path.Steps()) < 1 {
354+
if path == nil || len(path.steps) == 0 {
300355
return in, path, nil
301356
}
302357
stepper, ok := in.(AttributePathStepper)
@@ -306,11 +361,11 @@ func WalkAttributePath(in interface{}, path *AttributePath) (interface{}, *Attri
306361
return in, path, ErrNotAttributePathStepper
307362
}
308363
}
309-
next, err := stepper.ApplyTerraform5AttributePathStep(path.Steps()[0])
364+
next, err := stepper.ApplyTerraform5AttributePathStep(path.NextStep())
310365
if err != nil {
311366
return in, path, err
312367
}
313-
return WalkAttributePath(next, &AttributePath{steps: path.Steps()[1:]})
368+
return WalkAttributePath(next, NewAttributePathWithSteps(path.steps[1:]))
314369
}
315370

316371
func builtinAttributePathStepper(in interface{}) (AttributePathStepper, bool) {

0 commit comments

Comments
 (0)