-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
inmem startup improvments #9164
Conversation
walkTags(buf, func(key, value []byte) bool { | ||
tags = append(tags, NewTag(key, value)) | ||
tags[p].Key = key |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jwilder this change removes a runtime.typedmemmove
@@ -281,8 +281,8 @@ func ParseKeyBytes(buf []byte) ([]byte, Tags) { | |||
return buf[:i], tags | |||
} | |||
|
|||
func ParseTags(buf []byte) (Tags, error) { | |||
return parseTags(buf), nil | |||
func ParseTags(buf []byte) Tags { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the error
return value was always nil
, so this removes a stack push / pop for every ParseTags
call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. @benbjohnson @e-dard might want to take a look. Not sure if this will affect their current dev branch.
type measurement struct { | ||
Database string | ||
Name string `json:"name,omitempty"` | ||
NameBytes []byte // cached version as []byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need both a string and a byte version of name? Wonder if we can just have Name []byte
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I'll take a look and adjust if Name
is no longer needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name
is still used in a number of places, however I pushed an additional commit that replaces []byte(Measurement.Name)
with Measurement.NameBytes
to prevent additional allocations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. Can remove the other places some other time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍 I just have a couple of small suggestions/questions.
Can we hold off merging this until Ben and I get the dev branch er-tsi-index-part
merged in?
tsdb/engine/tsm1/engine.go
Outdated
) | ||
|
||
func BlockTypeToInfluxQLDataType(typ byte) influxql.DataType { | ||
if int(typ) < len(blockToFieldType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we're on an über optimisation drive here, you could also have:
fieldTypesN byte = len(blockToFieldType)
...
...
...
func BlockTypeToInfluxQLDataType(typ byte) influxql.DataType {
if typ < fieldTypesN {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried that, but alas the bounds check elimination (BCE) optimization can't be used. We can only declare FieldTypesN
as a var
and the compiler cannot prove this value doesn't change outside the BlockTypeToInfluxQLDataType
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. 👍
tsdb/engine/tsm1/engine_test.go
Outdated
t := makeBlockTypeSlice(100) | ||
for i := 0; i < b.N; i++ { | ||
for j := 0; j < len(t); j++ { | ||
tsm1.BlockTypeToInfluxQLDataType(t[j]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure this can't get optimised away by the compiler? That used to be a problem so I always got into the habit of defining a var result influxql.Unknown
somewhere outside the benchmark.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch – if the function was not inlined, it would have made no difference. We can deduce the inner loop overhead is thus about 45ns :)
Original benchmark was about 212ns, less loop overhead is 167ns. I added an assignment and it jumped from 45ns to 70ns. So about 30ns vs 167ns or about a 5.5x improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still a good improvement.
tsdb/index/inmem/meta.go
Outdated
|
||
mu sync.RWMutex | ||
shardIDs map[uint64]struct{} // shards that have this series defined | ||
deleted bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move deleted
down to the bottom? There will be 7 bytes of padding after deleted
, and if it gets left here then we'll end up with a larger than necessary struct size if we add anything that's less than 8 bytes underneath.
t.mu.Unlock() | ||
} | ||
|
||
// StoreByte stores ids under the value key. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me why we need this new method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To complement the LoadByte
as there is a drive to move to []byte
keys vs string
keys. This change also removes a call to runtime.slicebytetostring
at the call site in measurement#AddSeries
. The cast in StoreByte
is for the map key, so we benefit from the compiler optimization that it skips the alloc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get the push to []byte
; it's something that's been slowly chipped away at over time but seems never ending 😄
The cast in StoreByte is for the map key, so we benefit from the compiler optimization that it skips the alloc.
That was kind of the underlying point of my confusion I guess.. We can't benefit from that particular compiler optimisation here because we need to alloc in order to guarantee that the map key is stable don't we? I'm pretty sure that particular optimisation would only work on the read-side wouldn't it?
With Store(value string, ids seriesIDs)
I would assume there would be a runtime.slicebytetostring
on the string(value)
at the call site but then there would be no further allocations would there? Or maybe there would be another allocation when we did t.valueIDs[value] = ids
in Store
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed you are correct. Whilst map's won't allocate if a space exists to store a new key in a bucket, the assignment does indeed cause an allocation every time. It seems the compiler is not smart enough to delay the allocation if the key does not exist.
However, that leads to another issue with this logic anyhow. The *measurement#AddSeries
method calls tagKeyValue#LoadByte
to fetch the seriesIDs slice and later overwrites with the StoreByte
method. This is a race condition, as if two concurrent processes mutated the slice, one would overwrite the other on the StoreByte
call. We know this won't happen, as it is only mutated via *measurement#AddSeries
that takes an exclusive lock on the measurement
. We should just remove all this complicated locking logic :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're referring to the lock on TagKeyValue
, it was added specifically to fix an existing race (I forget where).
Will hold off until @e-dard merges the |
440c8b6
to
be616a8
Compare
* only call ParseTags when necessary * remove dependency on inmem.Series in tsdb test package * Measurement and Series are no longer exported. Their use is restricted to the inmem package * improve Measurement and Series types by exporting immutable fields and removing unnecessary APIs and locks Reduced startup time from 28s to 17s. Overall improvement including #9162 reduces startup from 46s to 17s for 1MM series across 14 shards.
be616a8
to
e48b9d1
Compare
e48b9d1
to
ed207b5
Compare
This PR was merged as it still had a number of useful improvements, including cleaning up some |
only callParseTags
when necessarySeriesFile
, this optimization is no longer availableinmem.Series
intsdb
test packageMeasurement
andSeries
are no longer exported. Their use is restrictedto the
inmem
packageMeasurement
andSeries
types by exporting immutablefields and removing unnecessary APIs and locks
Reduced startup time from 28s to 17s. Overall improvement including #9162 reduces startup from 46s to 17s for 1MM series across 14 shards.Required for all non-trivial PRs