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

Make the flattener smarter about skipping fields #113

Closed
yosiat opened this issue Sep 28, 2022 · 23 comments
Closed

Make the flattener smarter about skipping fields #113

yosiat opened this issue Sep 28, 2022 · 23 comments

Comments

@yosiat
Copy link
Collaborator

yosiat commented Sep 28, 2022

Continuing the discussion from this issue: #108

Currently the flattener is fast and performs well on cases where the searched fields (from the patterns) are covering most of the event payload, in cases of large payloads and the searched fields are small subset of it, the flattener can be improved.

Proposed Solution and POC results

My proposed solution consists of maintaining paths index being in use (as a tree data structure) and POC flattener written using Jx.

The source resides here - https://github.com/yosiat/quamina/tree/flat-hack inside "paths.go" and "jx.go", it passes most of the existing flattener tests except the error handling.

Benchmark results:

goos: darwin
goarch: arm64
pkg: github.com/yosiat/quamina-flatenner
Benchmark_Quamina_JxFlattener-10                 2612365               437.9 ns/op            24 B/op          3 allocs/op
Benchmark_Quamina_Flattener-10                     83246             14347 ns/op             360 B/op         23 allocs/op
Benchmark_LargePayload_QuaminaFlattner-10           78272             15307 ns/op            1240 B/op         44 allocs/op
Benchmark_LargePayload_JxFlattner-10               876013              1365 ns/op             904 B/op         24 allocs/op
PASS
ok      github.com/yosiat/quamina-flatenner     5.727s

source: #108 (comment)
Those benchmarks can't be shared currently since they are using an internal data, but can be adjusted and shared if needed.

BigShellStyle benchmark (in benchmarks_test.go) -

# Quamina
Field matchers: 27 (avg size 1.000, max 1), Value matchers: 1, SmallTables 54 (avg size 15.500, max 28), singletons 0
428,547.72 matches/second with letter patterns

# Jx
Field matchers: 27 (avg size 1.000, max 1), Value matchers: 1, SmallTables 54 (avg size 15.500, max 28), singletons 0
1,367,947.02 matches/second with letter patterns

And in CityLots benchmarks where we are pattern matching against the whole event data, we see there is no difference -

=== RUN   TestCityLots
Field matchers: 7 (avg size 1.500, max 3), Value matchers: 6, SmallTables 0 (avg size n/a, max 0), singletons 6

173,434.09 matches/second

--- PASS: TestCityLots (1.64s)
=== RUN   TestCityLots_JX
Field matchers: 7 (avg size 1.500, max 3), Value matchers: 6, SmallTables 0 (avg size n/a, max 0), singletons 6

177,609.63 matches/second

Open Questions & Notes

  1. As discussed we won't use Jx in Quamina, since we don't want to use an external dependencies - it's being used only for POC purposes, and once we have a general idea on how to make skipping smarter & faster in large payloads we will implement it in existing flattener.
  2. Current discussion and open question is how to pass the information about needed paths to pluck.
@yosiat
Copy link
Collaborator Author

yosiat commented Sep 28, 2022

Replying to @timbray last comment on the former issue:

#108 (comment)

Instead of making NameTracker smarter, we could consider a Flattener implementation that took config options, for example "when you come to this field you can stop, nothing useful after it". Because it sounds like skipping parts of the event is only really useful when you have hand-crafted events and the caller has inside knowledge about the structure.

I am not sure how we can implement this part of you can stop, we don't which field is going to be the last in event JSON, this will reside on the assumption the events are hand-crafted. So can you elaborate on that part please?

Have you looked on my proposed paths tree data-structure? maintaining will be fairly simple as I already have done that (didn't considered deletions yet), it will be handed to the Flattener which can use this information to understand which fields to pluck (agnostic to JSON, can be used in other formats as well).

I conducted some benchmarks, and in my case of events like:

{
    "context": { /* small list of properties, <10 */ },
    "payload": { /* large object, nested one here */ }
}

I am doing pattern matches against context fields and some payload fields, and I see improvements over the existing flattener as well, I will be doing more benchmarks and will anonymize them so I can share them for further discussion.

@timbray
Copy link
Owner

timbray commented Sep 28, 2022

Here's a question: Is it true that this optimization only works when the fields that you want to match are concentrated in the leading bytes of the event data?

Maybe do a benchmark to see if jx is still helpful when the matching fields are randomly distributed or in the trailing bytes? If so, then the answer to the question is No and that is super interesting and jx style should probably replace the current flatten_json scanner.

If yes, then we need to figure out what data needs to be passed to the jx-style scanner to get the optimization. My first suggestion was to identify some field at which the scanner can safely stop but I'm sure there are other good ideas. Quamina has a With* config API so once we figure out what the right parameters are it should be easy to add this and make it easily accessible to users.

@timbray
Copy link
Owner

timbray commented Sep 28, 2022

Oops, looks like we were writing comments in parallel…

I don't quite understand the paths tree idea, in your example are you restricting the flattening to the context top-level member of the event? So the config data would be to give the name of a top-level member and flatten only that?

@timbray
Copy link
Owner

timbray commented Sep 28, 2022

Anyhow, looking forward to seeing benchmark results.

@yosiat
Copy link
Collaborator Author

yosiat commented Sep 28, 2022

Maybe do a benchmark to see if jx is still helpful when the matching fields are randomly distributed or in the trailing bytes?

Just to make sure I understand this properly, trailing bytes you mean that the fields are at the end of object? If so, I am pretty sure that Jx will need to scan the whole object, and then we will compare just how fast it skips objects compare to existing one.

Anyhow, what you are suggesting I am going in the benchmarks I wrote above - I am doing pattern matches against context fields and some payload fields.

in your example are you restricting the flattening to the context top-level member of the event?

In the benchmarks I post on the original post, this what I did, but now, I am doing benchmarks to understand how is the performance when I am checking against 4 cases:

  1. Field which is the first inside payload
  2. Field which is the last inside payload - so essentially we need to skip to the end of the object.
  3. Field which inside an object property inside payload, but it's in the middle - payload.middle_object.id for example.

I'll post the benchmarks soon (today/tomorrow), I just want to find public data-set that I can use instead of my internal data.

@yosiat
Copy link
Collaborator Author

yosiat commented Sep 28, 2022

Ran the benchmarks of the 3 cases above with public data (I found from simdjson repository)

Benchmarks are here: yosiat@0dcf35f

To run, checkout this branch locally and run - go test -benchmem -run="^$" -bench "^Benchmark"

If you want me to conduct more benchmarks it will be easy now with the helpers I made, just let me know.

Context fields

Fields: context.user_id & context.friends_count

Flattening only:

name                                         time/op
_JxFlattener_ContextFields-10                 184ns ± 0%
_JsonFlattener_ContextFields-10              17.3µs ± 0%

name                                         alloc/op
_JxFlattener_ContextFields-10                 16.0B ± 0%
_JsonFlattener_ContextFields-10                560B ± 0%

name                                         allocs/op
_JxFlattener_ContextFields-10                  1.00 ± 0%
_JsonFlattener_ContextFields-10                23.0 ± 0%

Matching:

name                                         time/op
_JsonFlattner_Evaluate_ContextFields-10      18.3µs ± 0%
_JxFlattner_Evaluate_ContextFields-10         589ns ± 0%

name                                         alloc/op
_JsonFlattner_Evaluate_ContextFields-10        992B ± 0%
_JxFlattner_Evaluate_ContextFields-10          448B ± 0%

name                                         allocs/op
_JsonFlattner_Evaluate_ContextFields-10        33.0 ± 0%
_JxFlattner_Evaluate_ContextFields-10          11.0 ± 0%

Middle nested field

Field: payload.user.id_str

Flattening only:

name                                         time/op
_JxFlattener_MiddleNestedField-10            2.72µs ± 0%
_JsonFlattener_MiddleNestedField-10          17.4µs ± 0%

name                                         alloc/op
_JxFlattener_MiddleNestedField-10             16.0B ± 0%
_JsonFlattener_MiddleNestedField-10            576B ± 0%

name                                         allocs/op
_JxFlattener_MiddleNestedField-10              1.00 ± 0%
_JsonFlattener_MiddleNestedField-10            24.0 ± 0%

Matching:

name                                         time/op
_JsonFlattner_Evaluate_MiddleNestedField-10  18.1µs ± 0%
_JxFlattner_Evaluate_MiddleNestedField-10    2.96µs ± 0%

name                                         alloc/op
_JsonFlattner_Evaluate_MiddleNestedField-10  1.00kB ± 0%
_JxFlattner_Evaluate_MiddleNestedField-10      296B ± 0%

name                                         allocs/op
_JsonFlattner_Evaluate_MiddleNestedField-10    33.0 ± 0%
_JxFlattner_Evaluate_MiddleNestedField-10      8.00 ± 0%

Last field

Field: payload.lang_value

Flattening only:

name                                         time/op
_JxFlattener_LastField-10                    7.10µs ± 0%
_JsonFlattener_LastField-10                  17.7µs ± 0%

name                                         alloc/op
_JxFlattener_LastField-10                     16.0B ± 0%
_JsonFlattener_LastField-10                    544B ± 0%

name                                         allocs/op
_JxFlattener_LastField-10                      1.00 ± 0%
_JsonFlattener_LastField-10                    22.0 ± 0%

Matching:

name                                         time/op
_JsonFlattner_Evaluate_LastField-10          17.5µs ± 0%
_JxFlattner_Evaluate_LastField-10            7.20µs ± 0%

name                                         alloc/op
_JsonFlattner_Evaluate_LastField-10            824B ± 0%
_JxFlattner_Evaluate_LastField-10              296B ± 0%

name                                         allocs/op
_JsonFlattner_Evaluate_LastField-10            29.0 ± 0%
_JxFlattner_Evaluate_LastField-10              8.00 ± 0%

@yosiat
Copy link
Collaborator Author

yosiat commented Sep 28, 2022

I have done some basic investigation on existing flattener given those benchmarks, and got some interesting finds:

  • Most of the allocations are because of escaping text fields
  • Most interesting stuff: we are wasting time of reading string values for members we don't care, while instead we should skip those values.

I got to go sleep, but I am bit optimistic that I can some huge wins with reducing allocations and increasing the performance of flattener by actually skipping values instead of reading them.

Will update probably tomorrow or over the weekend with some results.

@timbray
Copy link
Owner

timbray commented Sep 29, 2022

  • Most of the allocations are because of escaping text fields

Yeah. The core idea of flattenJSON is that the whole event is a []byte and we want to have Field structures that also have []byte for the field value, so it tries to just make a sub-slice []byte for the values, which should only require allocating a slice structure. But we need the field values to be pure UTF-8 so we can run a byte-level state machine, so for any field that contains JSON \-escaping, we have to allocate a buffer and de-escape the JSON bytes into it. That simdjson sample data has lots of \-escapes in most fields so a lot of allocating.

The other thing flattenJSON does is enrich the Field structs with the ArrayPos map of where you are in the arrays, which is necessary to get correct results. I notice that the CityLots data has lots of arrays, but the simdjson does too.

You might find it helpful to read https://www.tbray.org/ongoing/When/202x/2022/06/10/Quamina-Optimizing to understand what flattenJSON does. Sorry, it's kind of long and has a lot of philosophical stuff at the front, but does eventually get into flattenJSON.

  • Most interesting stuff: we are wasting time of reading string values for members we don't care, while instead we should skip those values.

My problem is I don't know how to bypass a string in JSON without looking at each byte to find the terminating ".

Now I'm super curious how jx manages to allocate so little and go so fast.

Thanks for this work, it's valuable.

@yosiat
Copy link
Collaborator Author

yosiat commented Sep 29, 2022

Yeah. The core idea of flattenJSON is that the whole event is a []byte and we want to have Field structures that also have []byte for the field value, so it tries to just make a sub-slice []byte for the values, which should only require allocating a slice structure. But we need the field values to be pure UTF-8 so we can run a byte-level state machine, so for any field that contains JSON -escaping, we have to allocate a buffer and de-escape the JSON bytes into it. That simdjson sample data has lots of -escapes in most fields so a lot of allocating.

This makes sense, I think we should concentrate on not doing this escaping unless the field is required, which is the second point.

You might find it helpful to read https://www.tbray.org/ongoing/When/202x/2022/06/10/Quamina-Optimizing to understand what flattenJSON does. Sorry, it's kind of long and has a lot of philosophical stuff at the front, but does eventually get into flattenJSON.

I read this article (and other Quamina articles) and it was interesting read! I was thinking about the ArrayPos optimization.

My problem is I don't know how to bypass a string in JSON without looking at each byte to find the terminating ".
Now I'm super curious how jx manages to allocate so little and go so fast.

You can't skip a JSON string (or any other value) without scanning each byte, the idea is not to perform any []byte allocation doing so (and don't escape strings for example).

The whole skipping logic is in this code: https://github.com/go-faster/jx/blob/main/dec_skip.go, more specifically for strings they have skipStr method.

I think we need the same kind of methods in our flattener as well, so we have two modes (and we already have fj.skipping so we can know in which mode we are) -

  • reading a value - read string / read int / read array / etc.
  • skipping a value

What I am thinking right now, is a more a question for you Tim, do we want to invest more time in existing flattener and try mimicking Jx logic and have better skipping and efficiency around traversing, or we can just use Jx? I understand there is a dependency question around here.

If dependency (and it's dependencies) are the issue, maybe we can find a Jx replacement which don't have dependencies?

If you say, you want to invest in existing flattener and not use Jx, I'll totally accept it and will invest my time (I have good vacation coming up soon ;) ) in improving existing flattener.

@timbray
Copy link
Owner

timbray commented Sep 29, 2022

That's a really good idea, look at fj.skipping and avoid allocating/escaping for strings that will not be used. I suggest that:

  1. We do that, stop unnecessary allocation/skipping in flattenJSON
  2. check the benchmarks again

After that, if jx is still a lot faster, we should face the question of whether to take a dependency on jx or maybe just steal some of its code (assuming its license allows that). I really do think that Quamina will have a better future without dependencies.

@yosiat
Copy link
Collaborator Author

yosiat commented Sep 29, 2022

Sounds acceptable! I'll do a POC with existing flattenJSON and skipping to see if that improves the situation and will post here the benchmark results.

@yosiat
Copy link
Collaborator Author

yosiat commented Oct 3, 2022

Hey!

So implemented quick to code skipping on string values (yosiat@bd0c943) -

benchmark                                                old ns/op     new ns/op     delta
Benchmark_JsonFlattener_ContextFields-10                 17230         15985         -7.23%
Benchmark_JsonFlattener_MiddleNestedField-10             17392         16225         -6.71%
Benchmark_JsonFlattener_LastField-10                     17029         15847         -6.94%
Benchmark_JsonFlattner_Evaluate_ContextFields-10         17596         16434         -6.60%
Benchmark_JsonFlattner_Evaluate_MiddleNestedField-10     17840         16836         -5.63%

benchmark                                                old allocs     new allocs     delta
Benchmark_JsonFlattener_ContextFields-10                 23             3              -86.96%
Benchmark_JsonFlattener_MiddleNestedField-10             24             4              -83.33%
Benchmark_JsonFlattener_LastField-10                     22             2              -90.91%
Benchmark_JsonFlattner_Evaluate_ContextFields-10         33             13             -60.61%
Benchmark_JsonFlattner_Evaluate_MiddleNestedField-10     33             13             -60.61%

benchmark                                                old bytes     new bytes     delta
Benchmark_JsonFlattener_ContextFields-10                 560           48            -91.43%
Benchmark_JsonFlattener_MiddleNestedField-10             576           64            -88.89%
Benchmark_JsonFlattener_LastField-10                     544           32            -94.12%
Benchmark_JsonFlattner_Evaluate_ContextFields-10         992           480           -51.61%
Benchmark_JsonFlattner_Evaluate_MiddleNestedField-10     1000          488           -51.20%

And we are way better in terms of allocations! I am concentrating on LastField benchmark (since it's the test the skip all values), our biggest allocator is pathForChild -

(pprof) top
Showing nodes accounting for 7241.86kB, 100% of 7241.86kB total
Showing top 10 nodes out of 37
      flat  flat%   sum%        cum   cum%
 2050.25kB 28.31% 28.31%  2050.25kB 28.31%  runtime.allocm
 2048.05kB 28.28% 56.59%  2048.05kB 28.28%  github.com/timbray/quamina.pathForChild (inline)
 1184.27kB 16.35% 72.94%  1184.27kB 16.35%  runtime/pprof.StartCPUProfile
  902.59kB 12.46% 85.41%  1447.25kB 19.98%  compress/flate.NewWriter
  544.67kB  7.52% 92.93%   544.67kB  7.52%  compress/flate.(*compressor).init
  512.04kB  7.07%   100%   512.04kB  7.07%  runtime.(*scavengerState).init
         0     0%   100%  1447.25kB 19.98%  compress/gzip.(*Writer).Write
         0     0%   100%  2048.05kB 28.28%  github.com/timbray/quamina.(*flattenJSON).Flatten
         0     0%   100%  2048.05kB 28.28%  github.com/timbray/quamina.(*flattenJSON).readObject
         0     0%   100%  2048.05kB 28.28%  github.com/timbray/quamina.Benchmark_JsonFlattener_LastField
(pprof)

Now I am pretty optimistic that if:

  • I'll implement more skipping on all values (boolean, numbers, arrays, maybe objects?)
  • If we implement your idea that will allow us exit early (since if we fetched all fields)
  • If we implement cache that will return us the full path (instead of calling pathForChild)

We will be way faster and we won't need any external the dependencies.
Now we need to think on early exit & paths cache, to solve both issues namesUsed won't help us too much here, let's examine this pattern -

{
	"context": {
		"name": ["yosi"]
	},
	"payload": {
		"name": ["attias"]
	}
}

Our namesUsed keys will be ["context", "payload", "name"], the problems here is that we loose path aware context, so:

  • Early exit is not possible - we read context.name how do we know if we need to read payload.name ? we can solve this by pointing namesUsed to number, so it will be { "context": 1, "payload": 1, "name": 2 } every time we fetch a field, we decrement the value, if we reach all of them as 0 we can exit (how to implement that efficiently is interesting).
  • Path cache we can't - name is not path-aware, so it can be context.name OR payload.name

What do you think @timbray ? would love to hear your ideas here!

@timbray
Copy link
Owner

timbray commented Oct 3, 2022

And we are way better in terms of allocations! I am concentrating on LastField benchmark (since it's the test the skip all values), our biggest allocator is pathForChild -

Excellent! Although I will always be interested in the CityLots as well, but you already knew that. I'll have a close look at pathsForChild. The comment says that we need a copy for each field - that assumption should be re-examined, if it's true then it's hard to avoid per-field allocation.

Now I am pretty optimistic that if:

  • I'll implement more skipping on all values (boolean, numbers, arrays, maybe objects?)
  • If we implement your idea that will allow us exit early (since if we fetched all fields)
  • If we implement cache that will return us the full path (instead of calling pathForChild)

We will be way faster and we won't need any external the dependencies.

I'm glad to hear that, but not surprised. The existing flatten code is heavily profiled and I would be surprised if the alternative was much faster.

{
	"context": {
		"name": ["yosi"]
	},
	"payload": {
		"name": ["attias"]
	}
}

Our namesUsed keys will be ["context", "payload", "name"], the problems here is that we loose path aware context, so:

Ah, this is very interesting. You notice that namesUsed stores segments not complete pathNames. In Ruler, the in-AWS predecessor of Quamina, we used to store the whole pathName and we got a big speedup across a bunch of use cases by changing to segments. If you look at the flattening code, in almost 100% of cases you can access a segment without having to do any allocation because \-escaping is rare in field names. If you want to check based on the full path then you have to construct the full path at flattening time. Now, the downside of using segments is exactly what you discovered here, we get false positives in the case where a segment appears in two different paths.

At the time, our conclusion was that "segments appearing in multiple paths" were rare enough that we get better performance on average with the current approach.

Now, it's possible that our range of test data was weird because it was mostly AWS control-plane stuff and if you look at that you find very few places where you have a replicated segment as part of a field that you would want to match on.

By the way, congratulations, you have now become one of the few people in the world who is an expert on flattening hierarchical data structures.

@timbray
Copy link
Owner

timbray commented Oct 3, 2022

Hmm, now I'm thinking about this. When the events are large, the benefit of being able to skip large parts of it is very important, and might outweigh the benefit of using segment caching. An idea that I'm going to save here to think about, not sure if it's good: How about special treatment for top-level fields? I.e. if the Pattern is like

{ "context": { "name": [ "yogi" ] } } 

Then we could remember that the only top-level field worth looking at is context, and cheaply skip all the others?

@yosiat
Copy link
Collaborator Author

yosiat commented Oct 3, 2022

Although I will always be interested in the CityLots as well, but you already knew that.

I think that we won't see too much difference in CityLots, because we compared Jx and it wasn't faster, since this is a benchmark that does pattern on the whole payload.

Once we will get closer to PR I'll run CityLots to make sure I didn't introduced any regression in that case as well.

If you want to check based on the full path then you have to construct the full path at flattening time

I think that I actually solved that before with Jx and "Paths Index", this code I never constructed the full path at flattening time since I traversed the JSON graph together with my "Paths Index" graph. The question of my "Paths Index" is memory usage with large amount of patterns (that query different paths, worst case, but needs to be checked).

By the way, congratulations, you have now become one of the few people in the world who is an expert on flattening hierarchical data structures.

Thanks a lot :) I am doing this work because I think Quamina can replace something I purposely built and our payloads have the same structure as benchmarks: { "context": { small }, "payload": { large } }, but when I'll use Quamina I'll want to filter on attributes based on payloads (the checks their will be only exists).

Then we could remember that the only top-level field worth looking at is context, and cheaply skip all the others?

So I am thinking about my (near future) use-case of Quamina, I'll have two cases:

  • Most of my patterns are going to be based on context, so skipping payload will have huge benefit.
  • Other of the patterns that will have huge throughput (can't say exact numbers, but let's say 5k msgs per second) - I am going to check both context (values equal / values IN ..) and payload (values exists).

So I'll need both cases to be fast.

@timbray
Copy link
Owner

timbray commented Oct 3, 2022

OK, sounds like you have a clear path forward. Looking forward to seeing a PR. One remark (maybe I made before?): Seems reasonable to add some sort of config API to Flattener, telling it explicitly what can be skipped.

@yosiat
Copy link
Collaborator Author

yosiat commented Oct 3, 2022

So I think I will make two PRs for making it easier to review:

  • One for skipping values instead of reading them, like I already did for strings
  • Another for introducing paths index - this will allow us an elaborate discussion on my proposed solution and we will consider the trade-offs, and we may close it and make another one for better approach.

About the first PR, flatten_json.go is growing to be big files which maybe hard to reason about, I was thinking about separating it to multiple files, for example flatten_json.go (main file), flatten_json_skip.go for skipping values etc.. we can also put it in it's own directory (and have separate package, by go conventions) for it. What do you think about that?

@timbray
Copy link
Owner

timbray commented Oct 3, 2022

We experimented with various directory layouts and on recommendation of @embano1 ended up with the current flat structure, which is thought to be OK for libraries.

There is the factor that Flattener is an interface and it is quite likely that in the future we would have multiple flatteners for different media types (Avro, protobufs, etc) so maybe that's a reason? Micha, got an opinion?

Personally I don't dislike big files too much but I'm not religious on the subject.

@timbray
Copy link
Owner

timbray commented Oct 3, 2022

Oh, a question: What's the value of a separate implementation for readNumber and readLiteral and so on? They never do allocation, I think?

@embano1
Copy link
Collaborator

embano1 commented Oct 4, 2022

We experimented with various directory layouts and on recommendation of @embano1 ended up with the current flat structure, which is thought to be OK for libraries.

There is the factor that Flattener is an interface and it is quite likely that in the future we would have multiple flatteners for different media types (Avro, protobufs, etc) so maybe that's a reason? Micha, got an opinion?

Personally I don't dislike big files too much but I'm not religious on the subject.

Good comments. In general, I would not break up/design (package) structure by size but rather behavior. When we started there was only the JSON flattener and it worked well here. I guess, now that we're thinking about different implementations and a future-proof package layout, it makes sense to break up the flat hierarchy a bit, e.g. introducing a flattener package with sub-folders. Pretty common in the Go src code base, e.g. https://pkg.go.dev/encoding#section-directories.

We could to a refactor PR and see if the following is intuitive for users and does not introduce circular deps:

|- <quamina root>
|- flatten
       |- json
       |- jsonskip
       |- protobuf

Not sure if it makes sense to group the JSON ones together, i.e. same package but different files (I like that actually) or even more sub-folders (err, probably not that one)?

@yosiat
Copy link
Collaborator Author

yosiat commented Oct 4, 2022

Oh, a question: What's the value of a separate implementation for readNumber and readLiteral and so on? They never do allocation, I think?

Went over all the values reading (together with arrays & objects) and the only skipping we need are strings, so I'll push a PR only for that.

Regarding the multiple files/folders, I'll retract for now, since I am not sure there is a full need for that and I agree with @embano1 suggestion for the future when we will have multiple flatteners to have separate directories.

@yosiat
Copy link
Collaborator Author

yosiat commented Oct 4, 2022

Made some interesting progress!

Implemented a functionality that will allow us skipping blocks (arrays & objects), it have no affect on allocations, but have a dramatic effect on runtime.

name \ time/op                               results/00_baseline  results/01_skip_strings  results/03_skip_arrays  results/04_skip_objects
_JsonFlattener_ContextFields-10                      17.3µs ± 0%              15.9µs ± 0%             13.8µs ± 0%               8.8µs ± 0%
_JsonFlattener_MiddleNestedField-10                  17.4µs ± 0%              16.1µs ± 0%             14.3µs ± 0%              10.9µs ± 0%
_JsonFlattener_LastField-10                          17.1µs ± 0%              15.8µs ± 0%             13.8µs ± 0%               9.5µs ± 0%
_JsonFlattner_Evaluate_ContextFields-10              17.6µs ± 0%              16.4µs ± 0%             14.3µs ± 0%               9.2µs ± 0%
_JsonFlattner_Evaluate_MiddleNestedField-10          17.9µs ± 0%              16.6µs ± 0%             14.5µs ± 0%              11.6µs ± 0%
_JsonFlattner_Evaluate_LastField-10                  17.5µs ± 0%              16.1µs ± 0%             14.0µs ± 0%               9.7µs ± 0%

name \ alloc/op                              results/00_baseline  results/01_skip_strings  results/03_skip_arrays  results/04_skip_objects
_JsonFlattener_ContextFields-10                        560B ± 0%                 48B ± 0%                48B ± 0%                 48B ± 0%
_JsonFlattener_MiddleNestedField-10                    576B ± 0%                 64B ± 0%                64B ± 0%                 64B ± 0%
_JsonFlattener_LastField-10                            544B ± 0%                 32B ± 0%                32B ± 0%                 32B ± 0%
_JsonFlattner_Evaluate_ContextFields-10                992B ± 0%                480B ± 0%               480B ± 0%                480B ± 0%
_JsonFlattner_Evaluate_MiddleNestedField-10          1.00kB ± 0%              0.49kB ± 0%             0.49kB ± 0%              0.49kB ± 0%
_JsonFlattner_Evaluate_LastField-10                    824B ± 0%                312B ± 0%               312B ± 0%                312B ± 0%

name \ allocs/op                             results/00_baseline  results/01_skip_strings  results/03_skip_arrays  results/04_skip_objects
_JsonFlattener_ContextFields-10                        23.0 ± 0%                 3.0 ± 0%                3.0 ± 0%                 3.0 ± 0%
_JsonFlattener_MiddleNestedField-10                    24.0 ± 0%                 4.0 ± 0%                4.0 ± 0%                 4.0 ± 0%
_JsonFlattener_LastField-10                            22.0 ± 0%                 2.0 ± 0%                2.0 ± 0%                 2.0 ± 0%
_JsonFlattner_Evaluate_ContextFields-10                33.0 ± 0%                13.0 ± 0%               13.0 ± 0%                13.0 ± 0%
_JsonFlattner_Evaluate_MiddleNestedField-10            33.0 ± 0%                13.0 ± 0%               13.0 ± 0%                13.0 ± 0%
_JsonFlattner_Evaluate_LastField-10                    29.0 ± 0%                 9.0 ± 0%                9.0 ± 0%                 9.0 ± 0%
  • results/00_baseline - latest main commit
  • results/01_skip_strings - what I presented above
  • results/02_skip_arrays - skipping the whole array values
  • results/03_skip_objects - skipping objects

I'll work on making this as a pull request (tests, coverage, refactors) and will post it soon (today/tomorrow)

Update: Posted a PR: #119

@timbray
Copy link
Owner

timbray commented Nov 28, 2022

Improved in c117dab

@timbray timbray closed this as completed Nov 28, 2022
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

3 participants