ImmutableSet doesn't seem like the perfect fit for the RangeSet
data structure. It is good for saving memory in a persistent
setting, but not for the case when the population of the container
is tiny. This commit replaces RangeSet implementation and
redesigns the most common operations to be more efficient.
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
88 | Just curious - if they mostly contain 1 or 2 elements, why is N == 4 here? |
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
88 | Essentially, the main factor is intuition ๐ |
If the numbers are confirmed to be as good as what i've sneak-peeked so far, that should be pretty amazing. Also whoa, test coverage!~
WDYT about moving introducing a generic "SmallImmutableSet" in llvm/ADT to back your implementation?
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
88 | Given that you pass those by pointers, i suspect you don't need to fix the size at all. In fact the small-size of the small vector is, by design, a runtime value and you can use llvm::SmallVectorImpl * instead, which can point to SmallVector of any small-size. This would allow you to allocate small vectors of variable length which you can take advantage of whenever you know the length in advance. | |
135 | "But what about setGet()???" - some user, probably :) | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
119 | Given that we're certain from the start that the container will be persistent, can we save a copy by directly asking the factory to allocate a fresh container? Also this seems to be one of the cases where variable-sized small vectors would make sense. | |
281 | Thanks a lot for adding those comments. These methods were confusing as heck. | |
clang/unittests/StaticAnalyzer/RangeSetTest.cpp | ||
206 | Why is it necessary to specify this-> here? |
I'll add the charts with performance in the next couple of days
WDYT about moving introducing a generic "SmallImmutableSet" in llvm/ADT to back your implementation?
Oof, it is not really possible. First of all, existing RangeSet didn't really ensure that ranges do not intersect (add is/was not checking for that), and this is left for the user. Additionally, it is designed and optimized specifically for RangeSet queries and operations. Like intersect and contains in a more generic Set sense mean entirely different things.
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
88 | Sounds reasonable, I'll give it a shot! | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
281 | ๐ | |
clang/unittests/StaticAnalyzer/RangeSetTest.cpp | ||
206 | It is due to shenanigans in test fixtures implementation. I didn't find a better workaround and other tests for other components also use this-> as far as I checked :-( |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
---|---|---|
119 | Surprisingly this is not the case, it is cheaper to do whatever you want on small vectors on the stack and check if we already allocated it. |
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
88 | On the second thought, it's not going to be pretty. I still will need to allocate SmallVector<..., N>, where N is a compile-time constant, so it would be a big old switch where I "convert" run-time values into compile-time values so-to-speak. Additionally, the involvement of SmallVector doesn't seem to be necessary at all because in this setting we don't use the main feature of it that it can grow larger than this size onto the heap. So, we can simply do the same trick with ArrayRef and std::array, but with less ambiguity. However, one of the benefits of the current approach is "let's allocate as little as possible". It does the operation, checks if we allocated space for a container with these contents, and allocates it only if not. It is MUCH cheaper to do the operation and find out that we already have one like this or copy it to the newly allocated place than allocating it every time. |
This is a huge change, I've got pretty tired at the end of the review.
I haven't checked the test-cases too deeply.
I've found only minor typos, clarity enhancement opportunities etc. nothing serious.
It was awesome to see how this code can be improved, even becoming comprehensible.
Sorry for my nit spam if it needs further discussion - but it looks good to me.
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
35โ36 | I don't think we should inherit from standard type. It should be a private member instead. | |
51 | We have dump methods all over the place. Except this. | |
53โ55 | It's a good practice to define comparison operators as friend functions inline. | |
110 | I think it should be std::size_t. | |
118โ123 | What about merge or combine? I know that union is a keyword thus we can not use. | |
135 | Why don't we call this createSetOf? | |
149โ160 | I think an example would be awesome here. | |
170โ174 | Why does this operation take O(N) time? Shouldn't be O(logN) instead? | |
190โ193 | ||
195โ196 | ||
203 | As commented later, this function worth to be documented. | |
207โ208 | Why do we need to explicitly spell the RangeSet here? | |
210โ211 | ||
213โ215 | ||
222 | class Factory is a nested class of RangeSet, so it can already access all private parts. | |
225โ228 | If this is the Rule of 5, where is the defaulted destructor? | |
252โ259 | Finally, we got this, awesome! | |
261 | ||
269 | If we implement equality, we should also support inequality. | |
272โ273 | Missing explicit. | |
275โ276 | TBH, I don't even know what the pin function does. | |
277 | If it's a read-only operation why don't we take by value? Just like contains does. | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
146โ149 | Off-topic: Why do the Allocate and the similar allocator functions return T* if there is NO living object there (yet). | |
177โ178 | ||
181โ188 | Previously we had an early empty check here. | |
281 | I agree. These are black magic. xD | |
322โ325 | It could definitely bear a longer name. | |
367โ370 | ||
375 | It's unfortunate to refer to the next subrange by the ++Second. | |
384 | It's probably a personal taste though. | |
412โ417 | What.size() seems not justified here. | |
431 | ||
498 | Can we use range-based for loops? |
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
110 | Yeah, I didn't see a unified stand of the community on this matter. It looks like both options are pretty widespread. I mimicked SmallVector.h, but I can easily change it. | |
118โ123 | Maybe, but the real union function is not present. And I think that in the original code it is add to hint that it's not really a union and nothing complex is done here. Maybe something like mergeUnchecked would work. |
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
149โ160 | Agree, I simply copy-pasted the comment, but it would be better to expand it a bit because it might be confusing. | |
170โ174 | We still create a copy of an old vector to maintain persistence, that's why it's O(N). | |
190โ193 | The same thing with this comment, I think I'll rework it even more | |
195โ196 | +1 | |
203 | I didn't think that it's necessary because of the fact that it's private, but it's pretty important, so I'll do it. | |
207โ208 | Good point! | |
210โ211 | +1 | |
213โ215 | +1 | |
225โ228 | It is, and it is forgotten, good catch! | |
269 | Sure! | |
272โ273 | More like missing /* implicit */ because it is intentional | |
277 | Good point. Essentially it modifies the argument the same way pin functions do - by changing its type. I'll add the comments | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
146โ149 | 100% agree. It might be a source of mistakes where the type system doesn't help. | |
177โ178 | I don't really see the benefits of one way over another and as I said in another comment, I don't really like to pay for an extra copy with iterators as just a rule of thumb. | |
181โ188 | I'll add assertions checking for non-emptiness. | |
322โ325 | I think that there is nothing wrong in spelling out std::swap twice. | |
367โ370 | OK | |
375 | NP | |
384 | It basically makes it Second++, and even though for these iterators it doesn't really matter I prefer not to pay extra copy when I don't need it. | |
498 | We sure can, I wanted to change it but forgot |
Here are some benchmarking results.
In docker:
Natively on my Mac (no memory measurements due to this issue during psutil installation):
These are really promising figures, nice work! (And the measurement stuff itself is also a great addition, thanks for that!)
Not that it would matter much, but I was just wondering why there is a slightly bigger memory usage in some of the docker/small projects? The most conspicuous is oatpp.
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
118โ123 | Now I get it. Let's keep it as-is. | |
170โ174 | Thanks. BTW I just forgot about this comment :D | |
272โ273 | It doesn't have a too long identifier. | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
177โ178 | Iterators supposed to be cheap to copy. That is why we take them by value all over the place. To prove it, I disassembled the following functions in release clang build: // _Z21raw_rangeset_iteratorN5clang4ento8RangeSetE const llvm::APSInt &raw_rangeset_iterator(RangeSet What) { RangeSet::iterator It = What.begin(); --It; return It->To(); } // _Z26std_prev_rangeset_iteratorN5clang4ento8RangeSetE const llvm::APSInt &std_prev_rangeset_iterator(RangeSet What) { RangeSet::iterator It = What.begin(); return std::prev(It)->To(); } (gdb) disassemble _Z21raw_rangeset_iteratorN5clang4ento8RangeSetE Dump of assembler code for function raw_rangeset_iterator(clang::ento::RangeSet): 0x0000000006102b10 <+0>: mov (%rdi),%rax 0x0000000006102b13 <+3>: mov -0x8(%rax),%rax 0x0000000006102b17 <+7>: retq End of assembler dump. (gdb) disassemble _Z26std_prev_rangeset_iteratorN5clang4ento8RangeSetE Dump of assembler code for function _Z26std_prev_rangeset_iteratorN5clang4ento8RangeSetE: 0x00000000061040d0 <+0>: mov (%rdi),%rax 0x00000000061040d3 <+3>: mov -0x8(%rax),%rax 0x00000000061040d7 <+7>: retq End of assembler dump. | |
322โ325 | That's a good question :) |
Thanks ๐
Not that it would matter much, but I was just wondering why there is a slightly bigger memory usage in some of the docker/small projects? The most conspicuous is oatpp.
The measurements fluctuate quite significantly and as you can see the means for both old and new experiments for oatpp are pretty close. So, I would say that the memory differences are not conclusive.
This being said, the performance measurements should also be taken with a grain of salt. We can't really say that we get 5-10% performance improvement for sure, but that analysis tends to be faster instead.
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
53โ55 | It doesn't seem like there is a single opinion on this in the codebase. | |
135 | The naming is this way to be consistent with ImmutableSet::Factory | |
272โ273 | These constructors are private and used in Factory methods. IMO they make those methods less cluttered. | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
322โ325 | I'm sorry but I still don't get the importance of dropping std:: here. |
Previously, I liked this. Now I love it!
The benchmarks look promising as well.
I think I can not convince you about not modifying iterators using ++,--, etc. outside of loop-expressions. So be it :D
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
51 | How about marking this LLVM_DUMP_METHOD? | |
53โ55 | Okay. | |
106 | Shouldn't we call this const_iterator? | |
135 | Makes sense. | |
205 | ||
207 | ||
268 | Btw in C++20 we will have a way expressing this: explicit(false) | |
271โ280 | Aww, excellent. Thanks. | |
272โ273 | Okay. | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
322โ325 | Since we don't rely on Argument Dependent Lookup in this particular case, both versions do the same. | |
414 | ||
462โ470 | It might be overkill, but we can do this using transform. | |
495โ507 | Hmm, we could simplify this further, assuming Range::operator<< is defined. |
Here is my five cents. I haven't done with the review yet. I'm gonna return to it a bit later.
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
58 | I'm not sure about optimizations but it seems like it could have less commands by omiting this. | |
135 | IMO function's name should match to what it does. If it creates smth, so its name should starts with create. Even if all other functions have misleading names it doesn't mean we are delegated to ignore this fact. |
I didn't check correctness of each Factory function but if it passes all tests and improves performance, I think it's enough.
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
73โ74 | That's very usefull description for reviewers, but I think is redundant for newcomers, when you're reading this you can't understand why it compares to ImmutableSet at all. I think this preamble only relates to the Summary of this patch as the core reason of this change. | |
129 | We can add one more instance of add (and maybe others): RangeSet add(RangeSet original, llvm::APSInt &point);. It could simplify function RangeSetTest::from in tests and be useful across the code on the whole. | |
174 | IMHO you should change params with each other: | |
clang/unittests/StaticAnalyzer/RangeSetTest.cpp | ||
105 | Explain, please, what's the matter to use wrap everywhere, not just call checkNegateImpl explicitly? this->checkNegateImpl({{MIN, A}}, {{MIN, MIN}, {D, MAX}}); | |
185 | Thank you for making it in a proper way, instead of a list with different type instantiations. |
@vsavchenko
Hi, I actually want this patch goes developing and be loaded. It's really the worth one. Is it abandoned?
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
---|---|---|
433โ434 | What is a persistent range set, so moving it or making it persistent won't do, but we indeed can simply return What. |
Oof, it took me quite some time to come back to this.
I don't think that I'll be able to gather more performance-related data than I already did, so if it's OK with y'all, we can land it.
@NoQ @steakhal @ASDenysPetrov ?
clang/include/clang/StaticAnalyzer/Core/PathSensitive/RangedConstraintManager.h | ||
---|---|---|
73โ74 | I don't want to fully remove this comment because this is a comment explaining implementation detail, which I believe newcomers can skip altogether. | |
205 | That's a bit too verbose for the header IMO. | |
207 | The same as above. I don't want to mention caches or arenas for the sake of hiding those as implementation detail. One might rightfully argue that having these methods here also leaks implementation detail into the header file, but without a pImpl, which I believe is not very widespread in the LLVM codebase, it's not possible to hide these. | |
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
462โ470 | Yeah, I'd say that it is wordier. | |
495โ507 | I think it looks a bit clearer with llvm::interleaveComma | |
clang/unittests/StaticAnalyzer/RangeSetTest.cpp | ||
105 | Sure, we want to use a "raw" version of ranges and range sets everywhere in the tests for the simplicity of the test. Here I got bored writing from(Arg0), from(Arg1), ... and went with this. | |
185 | Oh yes, it would've been a true nightmare repeating every test for every type |
Ah, I wanted to give it a go, but the bots caught an assertion failure for the parent revision of this. See the details at the Bugzilla ticket.
What is more concerning is that this patch resolves that assertion failure. I guess it implies it can not be NFC?
Regardless, I'm gonna wait to get that issue fixed before I have a deeper look.
clang/unittests/StaticAnalyzer/RangeSetTest.cpp | ||
---|---|---|
186 | You should probably test _ExtInts as well. |
Given that it did not change any reports in our testbench it seems to be safe to land it.
It clearly improves the API significantly, so I'm not opposing.
Really good work @vsavchenko.
PS: If should support _ExtInts as well, even if they are not too common.
Please try to fill this gap, before you push your changes.
clang/lib/StaticAnalyzer/Core/RangeConstraintManager.cpp | ||
---|---|---|
495โ507 | Awesome! |
Thanks ๐
PS: If should support _ExtInts as well, even if they are not too common.
Please try to fill this gap, before you push your changes.
I do have a bit of a struggle here. This is a unit-test and thus should be compiled for all of the supported architectures by all of the supported compilers.
Is there a __has_feature or something for me to check if _ExtInt can be used?
Aa, I get it. It is actually a clang specific extension. By checking git blame, it seems to be a quite new feature, clang-11+.
What about something like this?
using IntTypes = ::Types<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t #if defined(__clang__) && __clang_major__ >= 11 , __int128_t, __uint128_t , signed _ExtInt(200) , unsigned _ExtInt(200) #endif >;
https://godbolt.org/z/GfMfWEz88 Seems to work for a number of compilers.
This is not only a compiler feature, it also should be supported by the target architecture:
https://godbolt.org/z/ddjYYx9x6
It's getting complicated then xD. I guess we should complement unittests with LIT tests?
You can know there that clang is recent enough and pinning the target triple would also solve this.
I don't think we should inherit from standard type. It should be a private member instead.
https://quuxplusone.github.io/blog/2018/12/11/dont-inherit-from-std-types/#never-ever-inherit-from-any-std