6 ms·
The problem is inherent in the imperative style. The programmer SPECIFICALLY ASKED for all the partial results, and it turns out that doing what was asked is s
by beagle3 9y ago
The problem is inherent in the imperative style.
The programmer SPECIFICALLY ASKED for all the partial results, and it turns out that doing what was asked is sub-optimal, to the tune of O(n) allocations of O(n) each and O(n^2) runtime as opposed to the optimal O(1) allocations of O(n), and O(n) runtime.
It is extremely hard to make the compiler transform the sub-optimal version to the optimal one in the general case. Sure, one can special case strings (or std::string, or whatever) and do that without changing the semantics -- Python, for example, does that with string appends. However, none of Python, C++ or Rust will do that for a user-defined type.
The right thing is to make sure that idiomatic way to concatenate strings is, in fact, efficient - Python does that by making the idiom "seperator.join(string_list)", which is as efficient as it can be.
There's a lot of performance gains to be made by transforming unreasonable code to reasonable code, which is why compilers still do it; and unreasonable code just as often results from mountains of abstractions as it does from programmer unawareness. But IMHO our goal should be that the norm is that programmers are expected to write reasonable code, not that compilers are able to optimize unreasonable code.
- wruza 9y ago>"seperator.join(string_list)", which is as efficient as it can be. If you store strings in array, it is not efficient to concatenate it at all. Strings could be implemented as an arrays or strings internally, so none of (insert, delete, append, prepend) series would impact performance so much. Editors actually do that, and your text buffer is not a single string that memmoves every time you type a char. Strings as they are today almost everywhere are simply of bad design.
- beagle3 9y agoDepends on your use case. If you use a rope or tree representation (like most editors do these days), random access is O(log n) at best, in many implementations typically O(sqrt n) or even O(n), whereas concatenation string is O(1). I don't think that it is bad as a general compromise between editor buffers (lots of inserts and deletes and concats) and symbols (immutable after created), the vast majority of which are under 200 chars (file names, urls, actions, text fields). Sure, life would be better if every standard library supported a variety of string types for specific uses. But they don't, and I suspect that it is mostly for cognitive overload (that is, it is a PEBKAC, not a technical issue)
- wruza 9y agoMuch this, use case. Statistically, my use case is 95% cat cat cat cat... (hundreds of them) and then seqwrite of some form, never accessing it randomly. I'm generating json, xml, sql, etc to send it over socket. Sometimes it is utility library, sometimes me (or me writing utility library). My string type is always an array, but this type-shift is not obvious to new coders. "Why StringBuilder" was once very popular on SO, not sure for now. I oversimplified editors example, their structures are not for 'everyday' use, but I think that simple linked list in string would wipe the entire obvious class of cat-in-loop performance issues.
- majewsky 9y ago> Statistically, my use case is 95% cat cat cat cat... (hundreds of them) If you use a linked-list representation like you proposed, your performance would die a death by 1000 allocations. A simple string class backed by an exponentially growing contiguous memory buffer (i.e., what 99% of string classes do) sounds much better from a performance standpoint. Especially if you are able to know (or estimate) the size of your response, and allocate once upfront.
- wruza 9y agoI'm surely able to write a C routine that uses custom 1.5x-growing buffer and does everything, including subexpr indentation shift etc, in one pass. The point is that I cannot delegate this work to newbie/lowcost who will cat-cat-cat strings allocated by other newbie thousand times with no idea that it is slow and consuming. Even telling them how to do it properly I'll spend more machine cycles than simply sending it in production. Even then, very source strings are not under my control and I'm no world dictator to tell a bunch of maintainers who don't even know me what they should do for my performance case. It is real world, not my cool homegrown toolkit. It is not how-to talk, it is why not make already inefficient method slightly more forgiving by appropriate means. If you don't like a list, take an array, but I like to see tests against it before taking that "death" argument. Maybe tomorrow I'll test all three (multicat, list, array) of immutable strings here.
- burntsushi 9y ago> Strings as they are today almost everywhere are simply of bad design. I very much disagree. Eschewing a contiguous memory representation in favor of making other operations cheaper is a monumental trade off, and whether it's good or bad design depends on your use case. Others have mentioned that you lose random access (which is incredibly important for some algorithms), but you also lose optimizations that rely on a contiguous memory representation. For example, SIMD optimized routines like memchr live at the heart of any substring search algorithm. If you can't make effective use of SIMD optimized routines in your substring search, then your implementation will likely be left in the dust. If you proclaim contiguous memory representations as "bad design," then users of your strings won't care about design when you tell them that only bad designs have fast substring search implementations.
- beagle3 9y agoI agree completely. Back in the 8086 days, Boyer Moore string search delivered significant speed up over linear memory scan, but nowadays nothing practically beats SIMD linear scans except in very specialized situations.
- burntsushi 9y agoIndeed. Many Boyer Moore implementations implement the skip loop by using memchr. But in practice, it's often faster to forgo Boyer Moore completely and heuristically select the rarest byte in the string you're searching, and then run memchr on that byte. You can even pick the second rarest byte and use that as a quick way to throw out false positives before doing a full string comparison. (The aforementioned optimization is one of the tricks ripgrep uses that occasionally causes it to be faster than GNU grep. GNU grep uses a traditional Boyer Moore substring search with memchr always applied using the last byte in the pattern.)
- wruza 9y agoPersonally I see no difference between the current statement of sep.join(list) is idiomatic and possible statement of s.flatten() before BM/SIMD is idiomatic except that join is ubiquitous and BM/SIMD searching is never heard of by a regular developer in modern boring language. Not to mention that few of us who aware can implement or use it properly. edit: searching/iterating methods could simply flatten() string on demand, because one doesn't search in semi-composed string [to trash performance back again]. If string doesn't have to be searched, but is for socket/file, writing methods could `s.fetch_next(BUFSIZ)` and skip huge concatenation before BUFSIZ-chunking-again at all. What's the reason to concatenate prematurely AND by hand, if it could be done automatically and only if needed?
- Veedrac 9y agoRust will actually optimize a chain of string additions to a chain of `push_str`s. This isn't hardcoded, just how addition is implemented. fn add(mut self, other: &str) -> String { self.push_str(other); self } https://github.com/rust-lang/rust/blob/master/src/liballoc/string.rs#L1749 https://github.com/rust-lang/rust/blob/master/src/liballoc/s... Of course, upfront sizing is still advantageous, but you avoid the pathological n² case.