Java StringBuffer and StringBuilder performance
alblue.bandlem.com
alblue.bandlem.com
I'm a C++ programmer mainly, surely all Java programmers know why StringBuffer is bad by now?
Article also has a pretty pointless microbenchmark.
Most synthetic microbenchmarks are questionable by default. Even worse when there's any kind of limited resource utilization such as thread synchronization involved.
Synthetic microbenchmarks assume all the resources are just for them. Whole CPU instruction and data cache, all synchronization bandwidth, all memory bandwidth, etc.
Use profiling instead of microbenchmarks.
You wouldn't necessarily know, or notice - your users would just experience occasional random crashes. How about just using StringBuffer unless you really (that 0.01% case) need the performance of StringBuilder? As you say, profiling is the right way to approach these things.
I just don't think this is the right place to do the synchronization. Wouldn't a Double box (that's really a "SynchronizedDouble") be silly?
Besides, can you think of any legitimate reason to concurrently append to a StringBuffer in the first place?
Because when writing new code you should opt-in to thread safety, not out. Collections are not concurrent by default, there is no `unsynchronized` or `non-volatile` keywords, etc. StringBuilder:StringBuffer::HashMap:ConcurrentHashMap. Also, if the performance benefit requires negligible upfront cost (e.g. providing length to ArrayList if known) you should do it and build it in your muscle memory as the default approach.
The problem is StringBuffer (unlike many other parts of the stdlib) was the only approach available for a long time so people need to change their default way of thinking. Luckily it's been a long time since 1.5 and basically everyone has changed.
Actually, I don't see why it would take a very smart compiler to do this kind of escape analysis and automatically and safely replace StringBuffer by StringBuilder where possible. The only large downside I can see of that is that it would require the compiler to know quite a bit of the semantics of the standard library.
If you refactor your code and pass the StringBuffer to (small) helper functions, doing this would be more work, but it would not get essentially harder.
With regard to the sufficiently smart compiler: it seems like you'd need every method the StringBuffer is passed to be final to do that kind of analysis.
In the case where you're writing:
public void foo(List<String> strings) {
StringBuffer x = new StringBuffer();
for (String s: strings) {
x.append(s);
}
}
You could conceivably do the analysis at compile time. But the whole idea behind the JIT is that you don't need to. If the code is hot, it'll get compiled, and you'll probably be fine thanks to removing all the synchronization, while if it's not hot no one cares.I wish C++ had a runtime that could do same, inline smaller virtual methods, function pointers and shared library (.dll/.dylib/.so) calls at runtime.
It could also do other transformations, like optimize those branches away it can prove to be always true or false. It could flatten most abstractions, removing redundant parameter checks and call frame setups/teardowns. It could also do CPU (instruction set) specific optimizations.
I think C++ runtime that does this partial JITting is possible, but not easy. However, the benefits are interesting. While getting there won't be easy, you can achieve faster than native performance.
Something like this has already been actually done. Some examples below.
QuaC: http://digitalassets.lib.berkeley.edu/techreports/ucb/text/C...
DynamoRio: DynamoRio had some early success JITting native binaries resulting faster than native performance. Unfortunately current versions are slower than native.
https://github.com/DynamoRIO/dynamorio).
https://dspace.mit.edu/bitstream/handle/1721.1/87441/5486358...
Because it's unsafe some portion of the time? I don't think it's controversial to say safety should be the default, unless it imposes undue constraints. We have plenty of experience with the alternative. See C.
> The main reason behind sensible defaults is that they suit most for the best benefit.
Are we not talking about some small percentage of performance loss in string concatenation in exchange for a program that doesn't crash in the case it's used in a particular manner with threads? Am I misunderstanding this?
> Whatever language you are familiar with, I can undoubtedly show you safer yet less convenient (and less frequently needed) ways to do things to which you could pose your same question.
Yes. And when you point out where the same API exists for two constructs with different names, that achieve the same end, with small performance differences between them, and one is safe and one can possible cause the program to crash in some edge cases, I will side with the safer way as a default.
It's not like we as an industry or hobby need help writing buggy software. We've accomplished that just fine for decades.
Yes, in the rare case of using string buffering across threads simultaneously. This "some" is so small as to have no practical benefits to default to the safe way. I don't know of any code that does it or would use it (and if that code did exist, you would opt in to the safety guarantees by using a different class for that rare purpose).
> Are we not talking about some small percentage of performance loss in string concatenation in exchange for a program that doesn't crash in the case it's used in a particular manner with threads? Am I misunderstanding this?
A bit. New code makes a conscious choice to be thread safe or not. I would not recommend anyone edit old code to improve performance. But for new code, thread safe string buffers are like thread safe anything else in the language, they are opt in because the default is assumed to not be thread safe unless you really mean it. All programming languages I know that support cross-thread racy invocations have a thread-unsafe-by-default approach to their stdlib. This is a reasonable approach/default for practical reasons.
> I will side with the safer way as a default
This is specifically why I used the word "practicality". In your mind I assume that there never should be such things as dynamic languages? If safety trumps all other forms of practical choice, your programming environment and library choice will be really limited. We can all theoretically tout we choose safety over everything else, but we all know we have to make practicality sacrifices for many reasons that are unrelated to safety.
In what way are dynamic languages "unsafe"? To be clear, what I mean here by unsafe, and everywhere I've used the word, is "can cause a crash or memory corruption", which I consider different than "not the output I expected", such as weird order of operations.
Can using one over the other cause a Java program to crash in certain circumstances? Then I advise the one with the possibility to crash not be the default, if at all possible. Is it extremely rare that it could cause a crash? Are we sure that will always remain the case? Could interesting developments later on the JVM make it markedly less rare (maybe some auto-threading loop constructs)? Perhaps we shouldn't use current common current behavior as a justification for what will be safe in the future?
Is it not a case of actual crashing or corruption, and just a non-intuitive ordering of operations than someone might expect? Well then we aren't really talking about the same thing at all, and I don't really care as much what the default should be, as a matter of industry best practices.
Math.floor("banana")
They might attempt to provide a sensible answer, such as NaN if I run the above in JavaScript. But that operation doesn't really have a sensible semantic definition and the choice to return NaN instead of crashing is somewhat arbitrary.I mean, really, all NaN is in any language is a signal that a mathematical operation was used with a set of inputs for which the operation does not produce a defined numerical result. In a sense, this is always a sign of an insufficiently expressive type system that can't prevent you from feeding a bad value (or bad combination of values) to the operation.
# perl -E 'say "1zero"+0 '
1
# perl -E 'say "zero1"+0 '
0Even if that does cause a crash, which is entirely dependent on the language, I think there's a clear delineation of whether something can be expected to crash or not based on the opt-in assumptions of the language you choose. If your language is not type-safe, you've already accepted that as a constraint ("types may not help you"). Should we expect addition to be implemented in some way that causes problems within the constraints you've already accepted? I think not.
Here's two specific examples of dynamic languages, one weakly typed (Perl), one strongly typed (Python), and how they are internally consistent.
# perl -E 'say 1 + "2" + "four" + 8'
11
# python -c 'say 1 + "2" + "four" + 8'
File "<string>", line 1
say 1 + "2" + "four" + 8
^
SyntaxError: invalid syntax
Each makes it clear what you can expect up-front. If python could occasionally crash from addition of two integers, or Perl would occasionally interpret an all alpha string as non-empty in numeric context, all for the name of being slightly faster, I would consider that the wrong thing to do as well. (Note: There are rules for how mixed alphanumeric strings are interpreted in Perl, but they are well defined).I can say exactly the same thing about StringBuffer vs StringBuilder, with the obvious substitution for the word "language".
The point of kodablah's post is that, if your stance is that safety should always be chosen over other practical concerns, then a natural conclusion is that you should by default be opposed to dynamic languages, as they trade type safety for programmer comfort (for lack of a better term).
EDIT: I also want to point out that "crashing" vs throwing an error is not a distinction worth making for this conversation. If your program stops executing for any reason that could be prevented by a type-checking system, then it seems reasonable to call a type-checking system a safety feature.
Sure. And if you think the the difference in developer efficiency between a dynamic language and a static one is less than 10%, but when the gains are small, as has been shown in the linked article, then why pay the cost of possible program failure for a less than 10% gain in string concatenation speed when you probably won't event see it?
> I also want to point out that "crashing" vs throwing an error is not a distinction worth making for this conversation. If your program stops executing for any reason that could be prevented by a type-checking system, then it seems reasonable to call a type-checking system a safety feature.
I disagree. Exceptions which can be caught are not comparable to memory corruption, unless you can ensure the corruption can not cause a segmentation fault (which may be the case here, I have no idea).
Nobody seems to be answering my question as to what type of problem the threading error is limited to in Java. I've made clear that any objection I have to the default is moot based on possible assurances that the JVM might provide, but I haven't had anyone clarify.
I actually think that dynamic languages have negative efficiency impact over long periods of time and large numbers of developers. (If that doesn't make it clear where I stand on static vs dynamic typing...)
> Exceptions which can be caught are not comparable to memory corruption...
I never compared exceptions to memory corruption, I compared them to crashes. I could also "catch" crashes with techniques such as watch-dog processes with very similar effects. Now if you really mean "crashes" as "program faults due to memory corruption", as opposed to "unexpectedly halting execution", then you're both using a niche definition and gave a bit of deceiving definition of "unsafe":
> To be clear, what I mean here by unsafe, and everywhere I've used the word, is "can cause a crash or memory corruption"...
I agree, but that does leave open the possibility of them being more efficient in some circumstances.
> I could also "catch" crashes with techniques such as watch-dog processes with very similar effects.
Without a system for handling the crash information in such a way that you can feed it back into the restarted program to retrieve prior that and recover from it, I don't really see how they are equivalent.
> Now if you really mean "crashes" as "program faults due to memory corruption", as opposed to "unexpectedly halting execution", then you're both using a niche definition and gave a bit of deceiving definition of "unsafe"
I mean either, for the entire program, not some executing branch. I think "crash" is fairly unambiguously accepted as "an unrecoverable problem within the current running context causing it to halt." An exception is not a crash, it's a problem, which if unhandled can lead to a crash. Perhaps I'm in the minority in my opinion on this, but I don't think so.
> I mean either, for the entire program, not some executing branch.
That's a pretty arbitrary line to draw. The difference between one thread acting as a watch-dog for another thread, or the same situation with processes could probably be made indistinguishable with the proper setup.
What you consider actionable and what is possibly actionable may not be the same thing. For example...
> You can always retry, but the bottom line is that there's never going to be a good answer to flooring a banana.
Except potentially asking the user to supply another number, if it happened to be user input. There's an example of an actionable exception to something you seem to consider having no programmatic resolution.
> That's a pretty arbitrary line to draw.
Not according to wikipedia. The following is under the Application Crashes subsection of Crash (Computing)[1]
An application typically crashes when it performs an operation which is not allowed by the operating system. The operating system then triggers an exception or signal in the application. Unix applications traditionally responded to the signal by dumping core. Most Windows and Unix GUI applications respond by displaying a dialogue box (such as the one shown to the right) with the option to attach a debugger if one is installed. This behavior is called "crashing". Some applications attempt to recover from the error and continue running instead of crashing.
Note the last sentence, which implies recovery is not crashing. But you can call bullshit on Wikipedia as source (because that's sometimes valid), I don't really care. This whole discussion has devolved to the point it's not useful to continue. You stated your preference to crash elsewhere, which I have a difference in opinion on, so I don't think any headway will be made there. We can just agree to disagree on what we think is a best default behavior for a language.
If so, that's less an issue of safety, as much as it is data integrity, which is to my mind a less important constraint to keep. In that case, an argument about defaults with respect to safety is a non-sequitur, so I don't really have a point to argue with respect to this example (if true).
stringBuffer.append("a").append("b"); // Thread 1
stringBuffer.append("c").append("d"); // Thread 2
The answer is, any of the 6 permutations of "abcd" such that "a" appears before "b" and "c" appears before "d". However, maybe what you really wanted was just the two permutations of "abcd" or "cdab".Just saying, "thread-safe!" doesn't answer the question of how it's thread-safe and what its behavior will be if multiple threads are contending for it. All it means is that the program won't crash or corrupt just because multiple threads are in the same place at once, which is not really all that useful a guarantee when you're looking at very expensive locking mechanisms to get there.
Does that mean you may need to figure out why your data looks weird at some later date? Possibly. But to me that much preferred to entire program failure in hard to reason about circumstances, or in memory corruption, which can lead to the same.
Given with strings you probably have data corruption no matter what, do you want crashes on top of that or not?
Which was the entire point of my post. "Thread-safe" does not guarantee "usage-safe", and using them interchangeably is a fallacy. If the synchronized StringBuffer does not provide the guarantees you need anyway, there's no point in paying the cost of using it.
> Given with strings you probably have data corruption no matter what, do you want crashes on top of that or not?
I just said that, yes, I would rather the program crash than silently corrupt data. I would rather it crash 100% of the time, but I'll take intermittent crashes instead. A crashed process can be restarted; it's very much harder to fix data once corruption hits. And if data corruption hits anyway, at least if I also have a crash I have an idea of where to look for the issue.
Thread safety isn't something you build up from tread safe components. It has to be designed in from the top.
Further, almost all string builder uses are trivially thread safe.
You might as well sprinke random and pointless synchronized blocks around your code. The magical synchronization fairy will give you a false sense of correctness.
Use cases for StringBuffer are very limited …I can’t even think of an example where I would use it.
[0] I would think this can realistically happen for StringBuilder calls, but I'm not 100% sure.
Wow, I had no idea how bad sampling profilers are on JVM. And apparently JVM profiling in general. So collecting a stack trace sample requires a thread being suspended at a safepoint. And a global safepoint requires suspending all threads? Sampler that needs to do that will surely not only miss real CPU time consumer but also significantly alter performance behavior.
Instrumentation is indeed often a pretty bad option as well.
Is it possible to resolve JVM stack traces captured from kernel side (Windows and Linux have mechanisms for this)? That would certainly get around any safepoint restrictions, right?
But this is seriously micro-opimtization only beneficial in the most strict of situations. Only in this crazy situations would I ever use this as justification to choose one approach over the other.
Separately, it's interesting to me chained calls can be more efficient. Personally I prefer these longer, multi-line type of statements from a readability perspective. So I'm happy it happens to be efficient.
However with a normal JVM the only way to know if its happening (AFAIK) is to look at the compilation logs or JIT'd assembly, bytecode isn't enough
In any case, I re-ran the benchmarks with -appendJvmArgs -XX:+OptimizeStringConcat and saw no significant difference in the timing on OSX. You can of course run the gist linked at the bottom and see for yourself whether there is an impact for your OS but there is JEP280 which is looking at generating invoke dynamic for Java 9 which will change the measurements again and make the -XX:+OptimizeStringConcat flag obsolete.