-
Notifications
You must be signed in to change notification settings - Fork 13.3k
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
Fix StreamString read O(n^2); improve range read performance #4584
Conversation
platform.local.txt
Outdated
@@ -0,0 +1,2 @@ | |||
compiler.c.extra_flags=-DASYNC_TCP_SSL_ENABLED=1 -DASYNC_TCP_SSL_BEARSSL=1 -DESPZW_DEBUG_LEVEL=1 -DESPZW_LOG_TIME=1 |
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.
Did you really mean to include this file?
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.
LOL, accident... maybe I should add this file to .gitignore?
This is a very interesting find, and could explain some wdt issues I've seen. |
Sure, let's say I have a service function which converts any input string to a new string only contains chars from the odd indices.
String gimmeOddChars(String &&instr) {
static StreamString ss;
String out;
ss = std::move(instr);
ss.reset();
int c = ss.read();
while (c >= 0) {
out += (char)c;
ss.read();
c = ss.read();
}
return std::move(out);
} Before this PR, without |
@Adam5Wu what about implementing StreamString::operator=()? It could forward to String::operator=() and then call its own reset(). |
Well, I think I did not phrase the 2nd caveat well. Not only assignments are affected. Then there are discussion #4551 and PR #4582 Having virtual function call in String would significantly reduce the likelihood vtable could be placed on flash. I think this may have too much performance implications and design constraints. I'd rather settle with a little bit of manual efforts. :D |
Another possible approach is, instead of
In this way we can safely override assignment operator without worry about inflicting unwanted virtual method in String. Also since it is an adapter, by convention user of the adapter is responsible for maintaining the content of the backend string, so any String manipulation is outside of the contract. It is not going to solve the problem (string manipulation can cause weird Stream behavior), but just pushing it aside, but the argument would be, these problems shouldn't be our (String-Stream adapter) responsibility in the first place -- the StreamString simply promised too much it cannot deliver. |
@Adam5Wu I'm not an expert on how things are handled under the hood by the compilers (ABI in this case), but my understanding is as follows: |
Yes, if we implemented a StreamString::operator =, without marking the String::operator = virtual, then the child class operator hides the parent class operator. But the hidding is superficial, it only works if the reference is the child class; if the reference becomes the parent class, then only the parent operator will be invoked, not the child. For example:
To truly override the behavior, no matter what reference is used to refer to the instance, we need to declare the method virtual in parent class, and use override keyword in child class methods. |
If the argument is that, we are only interested in the superficial hidding of parent class's operator, then a more suitable approach is encapulation (i.e. the adapter) instead of inheritance.
|
Now I understand what you had in mind, and you're right. The example you just showed above works with the current O(n2) code.
|
Template also have its weirdness. A good example is ArduinoJson's StaticJsonBuffer. It is written in template for very good reason.
The above function cannot be realized with any simple transformation that I know of... There may be some very ugly hack that can achieve similar effects with templated class, but I'd rather not getting into it... |
There is another way, but you may not like it either: Effectively String has some extra logic and field nobody can use except StreamString. Aesthetically, I think nobody will like this one. :( |
Maybe we should start a poll, ask everyone to vote what they think are evil:
And we choose the least evil solution. |
|
@devyte Ah yes, the template problem can be solved by ... more templates! :D What if I need to use this method as a callback? (Actually it is a callback in my design, forgot to mention it earlier) |
1 is too slow. ...that leaves 4, which makes my brain cry. @igrr @earlephilhower @d-a-v do any of you have insight for this? |
I suppose another option is possible: |
@Adam5Wu, @devyte - Maybe there is some middle ground here that's not as algorithmically pure but can work just fine without introducing the The thread's long, so maybe I'm missing something, but looks like nothing is blocking if we can open things up in String, too. How about lazy reset on a write to the string while updating WString.cpp to handle the Even just including a real |
@earlephilhower, that matches No. 4 of my previously suggested solution. |
I personally slightly prefer solution 5 or 6, which are breaking changes. However, I think the conceptual modeling of StreamString is somewhat problematic, which is the root of all these problems.
Fixing the root yields much cleaner code and easier maintenance, compared to adding more and more complications to try to make a bad idea approximately right. |
Ah, sorry, I did see it but didn't recognize it. It's getting a little long! I'm not seeing (4) as that bad, honestly, and not breaking == goodness. A nice dumb implementation has a tiny Compared to what happens if you end up doing a heap operation, feels like nothing in the common case. |
Yes, as I said:
Then, for release 3.0.0 we could pull 4 out, and implement the adapter to move forward cleanly. |
The stl stringstream implementation from |
Closing per #6979 (comment) |
I ran into a performance anomaly where decoding a base64 certificate is faster than reading the decoded DER blob from a StreamString.
When I inspect the operation, I found StreamString is reading the string from head to tail, byte by byte, and with each byte read, it removes it from the head, which triggers memmove of the rest of the string content 1 byte forward... such algorithm is O(n^2)
Arduino/cores/esp8266/StreamString.cpp
Lines 48 to 49 in 29580e8
This PR changed the internals of StreamString to overcome the slow read operation:
There is, however, a bit of caveat with this change:
reset()
method to do just that;reset()
call. Because the offset is in derived class while String is the base class, assignment of new string content will not automatically trigger offset reset (unless a significant amount of refactoring is done in the String class).