Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Marshaling slice of structs with one NaN value in the first struct fails slowly vs. encoding/json, also has huge error message #603

Open
michaelmdresser opened this issue Feb 24, 2022 · 1 comment

Comments

@michaelmdresser
Copy link

We've discovered an odd performance case in our use of jsoniter. In essence, jsoniter becomes very slow and outputs extremely long error messages when marshaling a slice of structs where the first struct contains a NaN float64. I suspect this generalizes to NaN values "early" in the slice and has something to do with jsoniter's implementation details. Golang's encoding/json fails fast in this situation, as you'll see in the example code.

Is this a bug in jsoniter's implementation? Is it fixable, or a known limitation?

cc @mbolt35 who found this behavior

Test code

package main

import (
	"encoding/json"
	"fmt"
	"math"
	"testing"

	jsoniter "github.com/json-iterator/go"
)

type Foo struct {
	Name string
	F float64
}

func makeTestData() []*Foo {
	n := 5_000
	data := []*Foo{}
	for i := 0; i < n; i++ {
		f := &Foo{
			Name: fmt.Sprintf("foo-%d", n),
			F: 43,
		}
		if i == 0 {
			f.F = math.NaN()
		}
		data = append(data, f)
	}

	return data
}

func Test_EncodingJSON(t *testing.T) {
	data := makeTestData()
	bs, err := json.Marshal(data)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("Bs length: %d", len(bs))
}

func Test_Jsoniter(t *testing.T) {
	data := makeTestData()
	bs, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(data)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("Bs length: %d", len(bs))
}

encoding/json test

→ time go test -v -run 'Test_EncodingJSON'
=== RUN   Test_EncodingJSON
    nan_test.go:51: json: unsupported value: NaN
--- FAIL: Test_EncodingJSON (0.00s)
FAIL
exit status 1
FAIL	github.com/michaelmdresser/play/jsoniternanslow	0.002s
go test -v -run 'Test_EncodingJSON'  0.28s user 0.07s system 220% cpu 0.157 total

go test -v -run 'Test_EncodingJSON' 0.28s user 0.07s system 220% cpu 0.157 total

jsoniter test

The error message is enormous, on the order of the size of the input array for marshaling. I am eliding some of the output with ...etc... so it is readable:

→ time go test -v -run 'Test_Jsoniter'    
=== RUN   Test_Jsoniter
    nan_test.go:60: []*main.Foo: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: 
...etc...
main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: Name: main.Foo.F: unsupported value: NaN
--- FAIL: Test_Jsoniter (0.17s)
FAIL
exit status 1
FAIL	github.com/michaelmdresser/play/jsoniternanslow	0.168s
go test -v -run 'Test_Jsoniter'  1.59s user 0.31s system 571% cpu 0.331 total

go test -v -run 'Test_Jsoniter' 1.59s user 0.31s system 571% cpu 0.331 total

The error message gets larger, and the marshaling slower, when the struct has more fields

(Some of the performance hit with more fields may be caused by my terminal emulator rendering more data, but I don't think it accounts for the bulk of the performance hit)

package main

import (
	"encoding/json"
	"fmt"
	"math"
	"testing"

	jsoniter "github.com/json-iterator/go"
)

type Foo struct {
	Name   string
	Field1 string
	Field2 string
	Field3 string
	F      float64
}

func makeTestData() []*Foo {
	n := 5_000
	data := []*Foo{}
	for i := 0; i < n; i++ {
		f := &Foo{
			Name:   fmt.Sprintf("foo-%d", n),
			Field1: "test",
			Field2: "something goes here",
			Field3: "?????",

			F: 43,
		}
		if i == 0 {
			f.F = math.NaN()
		}
		data = append(data, f)
	}

	return data
}

func Test_EncodingJSON(t *testing.T) {
	data := makeTestData()
	bs, err := json.Marshal(data)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("Bs length: %d", len(bs))
}

func Test_Jsoniter(t *testing.T) {
	data := makeTestData()
	bs, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(data)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("Bs length: %d", len(bs))
}
→ time go test -v -run 'Test_EncodingJSON'
=== RUN   Test_EncodingJSON
    nan_test.go:51: json: unsupported value: NaN
--- FAIL: Test_EncodingJSON (0.00s)
FAIL
exit status 1
FAIL	github.com/michaelmdresser/play/jsoniternanslow	0.002s
go test -v -run 'Test_EncodingJSON'  0.28s user 0.04s system 216% cpu 0.146 total
→ time go test -v -run 'Test_Jsoniter'
=== RUN   Test_Jsoniter
    nan_test.go:60: []*main.Foo: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: 
...etc...
main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: Field3: Field2: Field1: Name: main.Foo.F: unsupported value: NaN
--- FAIL: Test_Jsoniter (0.73s)
FAIL
exit status 1
FAIL	github.com/michaelmdresser/play/jsoniternanslow	0.728s
go test -v -run 'Test_Jsoniter'  7.23s user 1.27s system 970% cpu 0.875 total
@lemonlinger
Copy link

Also run into this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants