Skip to content

Go 1.23 upgrade check list #3553

@roman-khimov

Description

@roman-khimov

It's not yet released, so the issue is to be updated, but there is one thing I've discovered with go-ordered-json which affects #3089 handling in that some sorting changes better be postponed till 1.23 is live (and we can do this when it's the default for builds, not minimal supported). A relatively simple change there:

diff --git a/encode.go b/encode.go
index f971e42..24d207b 100644
--- a/encode.go
+++ b/encode.go
@@ -18,7 +18,7 @@ import (
        "math"
        "reflect"
        "runtime"
-       "sort"
+       "slices"
        "strconv"
        "strings"
        "sync"
@@ -680,7 +680,7 @@ func (me *mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
                        e.error(&MarshalerError{v.Type(), err})
                }
        }
-       sort.Slice(sv, func(i, j int) bool { return sv[i].s < sv[j].s })
+       slices.SortFunc(sv, func(a, b reflectWithString) int { return strings.Compare(a.s, b.s) })
 
        for i, kv := range sv {
                if i > 0 {
@@ -1110,23 +1110,16 @@ func fillField(f field) field {
        return f
 }
 
-// byIndex sorts field by index sequence.
-type byIndex []field
-
-func (x byIndex) Len() int { return len(x) }
-
-func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
-
-func (x byIndex) Less(i, j int) bool {
-       for k, xik := range x[i].index {
-               if k >= len(x[j].index) {
-                       return false
+func cmpFieldsByIndex(a, b field) int {
+       for k, ak := range a.index {
+               if k >= len(b.index) {
+                       return 1
                }
-               if xik != x[j].index[k] {
-                       return xik < x[j].index[k]
+               if ak != b.index[k] {
+                       return ak - b.index[k]
                }
        }
-       return len(x[i].index) < len(x[j].index)
+       return len(a.index) - len(b.index)
 }
 
 // typeFields returns a list of fields that JSON should recognize for the given type.
@@ -1228,21 +1221,24 @@ func typeFields(t reflect.Type) []field {
                }
        }
 
-       sort.Slice(fields, func(i, j int) bool {
-               x := fields
+       slices.SortFunc(fields, func(a, b field) int {
                // sort field by name, breaking ties with depth, then
                // breaking ties with "name came from json tag", then
                // breaking ties with index sequence.
-               if x[i].name != x[j].name {
-                       return x[i].name < x[j].name
+               var res = strings.Compare(a.name, b.name)
+               if res != 0 {
+                       return res
+               }
+               if len(a.index) != len(b.index) {
+                       return len(a.index) - len(b.index)
                }
-               if len(x[i].index) != len(x[j].index) {
-                       return len(x[i].index) < len(x[j].index)
+               if b.tag == false && a.tag == true {
+                       return -1
                }
-               if x[i].tag != x[j].tag {
-                       return x[i].tag
+               if b.tag == true && a.tag == false {
+                       return 1
                }
-               return byIndex(x).Less(i, j)
+               return cmpFieldsByIndex(a, b)
        })
 
        // Delete all fields that are hidden by the Go rules for embedded fields,
@@ -1274,7 +1270,7 @@ func typeFields(t reflect.Type) []field {
        }
 
        fields = out
-       sort.Sort(byIndex(fields))
+       slices.SortFunc(fields, cmpFieldsByIndex)
 
        return fields
 }

that is expected to be beneficial (no conversions to interface, no slice checks, stronger comparison function), leads to serious performance degradation:

goos: linux
goarch: amd64
pkg: github.com/nspcc-dev/go-ordered-json
cpu: AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics  
                       │  slices.pre  │             slices.post              │
                       │    sec/op    │    sec/op     vs base                │
CodeEncoder-16           675.7µ ±  1%   793.7µ ±  1%  +17.46% (p=0.000 n=10)
CodeMarshal-16           862.3µ ±  1%   928.5µ ±  1%   +7.67% (p=0.000 n=10)
CodeDecoder-16           3.720m ±  2%   4.287m ±  1%  +15.25% (p=0.000 n=10)
DecoderStream-16         86.03n ±  1%   90.56n ±  1%   +5.26% (p=0.000 n=10)
CodeUnmarshal-16         3.809m ±  1%   5.159m ± 13%  +35.44% (p=0.000 n=10)
CodeUnmarshalReuse-16    4.022m ± 10%   4.775m ±  3%  +18.73% (p=0.000 n=10)
UnmarshalString-16       46.14n ±  1%   57.25n ±  1%  +24.05% (p=0.000 n=10)
UnmarshalFloat64-16      42.96n ±  2%   53.66n ±  1%  +24.90% (p=0.000 n=10)
UnmarshalInt64-16        36.69n ±  2%   46.07n ±  2%  +25.57% (p=0.000 n=10)
Issue10335-16            54.79n ±  2%   70.75n ±  1%  +29.14% (p=0.000 n=10)
Unmapped-16              142.1n ±  1%   212.0n ±  2%  +49.14% (p=0.000 n=10)
NumberIsValid-16         8.996n ±  2%   9.850n ±  6%   +9.48% (p=0.000 n=10)
NumberIsValidRegexp-16   194.1n ±  1%   197.0n ±  4%        ~ (p=0.210 n=10)
SkipValue-16             7.786m ±  2%   7.598m ±  4%        ~ (p=0.105 n=10)
EncoderEncode-16         20.07n ±  1%   22.27n ±  1%  +11.01% (p=0.000 n=10)
geomean                  3.770µ         4.427µ        +17.43%

                 │  slices.pre  │              slices.post              │
                 │     B/s      │      B/s       vs base                │
CodeEncoder-16     2.675Gi ± 1%   2.277Gi ±  1%  -14.87% (p=0.000 n=10)
CodeMarshal-16     2.096Gi ± 1%   1.946Gi ±  1%   -7.13% (p=0.000 n=10)
CodeDecoder-16     497.5Mi ± 2%   431.6Mi ±  1%  -13.23% (p=0.000 n=10)
CodeUnmarshal-16   485.9Mi ± 1%   358.8Mi ± 15%  -26.16% (p=0.000 n=10)
SkipValue-16       266.0Mi ± 3%   269.7Mi ±  1%        ~ (p=0.218 n=10)
geomean            823.1Mi        720.4Mi        -12.48%

                       │   slices.pre    │               slices.post               │
                       │      B/op       │      B/op       vs base                 │
CodeEncoder-16             3.500 ± 71%       3.500 ± 100%       ~ (p=0.892 n=10)
CodeMarshal-16           4.000Mi ±  0%     4.000Mi ±   0%       ~ (p=0.331 n=10)
CodeDecoder-16           1.911Mi ±  2%     1.981Mi ±   1%  +3.64% (p=0.000 n=10)
DecoderStream-16           8.000 ±  0%       8.000 ±   0%       ~ (p=1.000 n=10) ¹
CodeUnmarshal-16         2.903Mi ±  0%     2.903Mi ±   0%       ~ (p=0.402 n=10)
CodeUnmarshalReuse-16    1.614Mi ±  0%     1.626Mi ±   0%  +0.78% (p=0.000 n=10)
UnmarshalString-16         304.0 ±  0%       304.0 ±   0%       ~ (p=1.000 n=10) ¹
UnmarshalFloat64-16        292.0 ±  0%       292.0 ±   0%       ~ (p=1.000 n=10) ¹
UnmarshalInt64-16          288.0 ±  0%       288.0 ±   0%       ~ (p=1.000 n=10) ¹
Issue10335-16              312.0 ±  0%       312.0 ±   0%       ~ (p=1.000 n=10) ¹
Unmapped-16                344.0 ±  0%       344.0 ±   0%       ~ (p=1.000 n=10) ¹
NumberIsValid-16           0.000 ±  0%       0.000 ±   0%       ~ (p=1.000 n=10) ¹
NumberIsValidRegexp-16     0.000 ±  0%       0.000 ±   0%       ~ (p=1.000 n=10) ¹
SkipValue-16               0.000 ±  0%       0.000 ±   0%       ~ (p=1.000 n=10)
EncoderEncode-16           0.000 ±  0%       0.000 ±   0%       ~ (p=1.000 n=10) ¹
geomean                                ²                   +0.29%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                       │  slices.pre   │             slices.post              │
                       │   allocs/op   │  allocs/op   vs base                 │
CodeEncoder-16            0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
CodeMarshal-16            17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=10) ¹
CodeDecoder-16           77.09k ± 0%     77.25k ± 0%  +0.21% (p=0.000 n=10)
DecoderStream-16          1.000 ± 0%      1.000 ± 0%       ~ (p=1.000 n=10) ¹
CodeUnmarshal-16         92.66k ± 0%     92.66k ± 0%       ~ (p=1.000 n=10) ¹
CodeUnmarshalReuse-16    77.19k ± 0%     77.34k ± 0%  +0.19% (p=0.000 n=10)
UnmarshalString-16        2.000 ± 0%      2.000 ± 0%       ~ (p=1.000 n=10) ¹
UnmarshalFloat64-16       2.000 ± 0%      2.000 ± 0%       ~ (p=1.000 n=10) ¹
UnmarshalInt64-16         1.000 ± 0%      1.000 ± 0%       ~ (p=1.000 n=10) ¹
Issue10335-16             3.000 ± 0%      3.000 ± 0%       ~ (p=1.000 n=10) ¹
Unmapped-16               4.000 ± 0%      4.000 ± 0%       ~ (p=1.000 n=10) ¹
NumberIsValid-16          0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
NumberIsValidRegexp-16    0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
SkipValue-16              0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
EncoderEncode-16          0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                              ²                +0.03%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

Because strings.Compare is intentionally deoptimized (https://pkg.go.dev/strings#Compare, https://go101.org/blog/2022-10-01-three-way-string-comparison.html). This is to change soon with golang/go@fd999fd, but for now that's what it is, good old sort.Slice wins.

Metadata

Metadata

Assignees

No one assigned

    Labels

    I4No visible changesS2Regular significanceU4Nothing urgentenhancementImproving existing functionalitygoGo language related

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions