Go detects concurrent read and write on map despite locks - go

I'm writing a simple caching mechanism which has an Add an Evict and a Search method. The Search is currently not implemented yet, so there's no need to worry about that.
There's a relatively large number of goroutines that call Add to add data and there's only one which runs in an evict loop to evict data. As soon as I put some serious traffic on it Go throws up saying there's a concurrent read and write access on the map metricCache, but I can't see how that can happen because there are locks around it. I'm using Go 1.7.
File mdata/cache.go:
57: func NewCCache() *CCache {
58: cc := &CCache{
59: lock: sync.RWMutex{},
60: metricCache: make(map[string]*CCacheMetric),
61: accnt: accnt.NewFlatAccnt(maxSize),
62: }
63: go cc.evictLoop()
64: return cc
65: }
66:
67: func (c *CCache) evictLoop() {
68: evictQ := c.accnt.GetEvictQ()
69: for target := range evictQ {
70: c.evict(target)
71: }
72: }
73:
74: func (c *CCache) Add(metric string, prev uint32, itergen chunk.IterGen) {
75: c.lock.Lock()
76:
77: if ccm, ok := c.metricCache[metric]; !ok {
78: var ccm *CCacheMetric
79: ccm = NewCCacheMetric()
80: ccm.Init(prev, itergen)
81: c.metricCache[metric] = ccm
82: } else {
83: ccm.Add(prev, itergen)
84: }
85: c.lock.Unlock()
86:
87: c.accnt.AddChunk(metric, itergen.Ts(), itergen.Size())
88: }
89:
90: func (c *CCache) evict(target *accnt.EvictTarget) {
91: c.lock.Lock()
92:
93: if _, ok := c.metricCache[target.Metric]; ok {
94: log.Debug("cache: evicting chunk %d on metric %s\n", target.Ts, target.Metric)
95: length := c.metricCache[target.Metric].Del(target.Ts)
96: if length == 0 {
97: delete(c.metricCache, target.Metric)
98: }
99: }
100:
101: c.lock.Unlock()
102: }
That's the error message:
metrictank_1 | fatal error: concurrent map read and map write
metrictank_1 |
metrictank_1 | goroutine 3159 [running]:
metrictank_1 | runtime.throw(0xaade7e, 0x21)
metrictank_1 | /usr/local/go/src/runtime/panic.go:566 +0x95 fp=0xc4216a7eb8 sp=0xc4216a7e98
metrictank_1 | runtime.mapaccess2_faststr(0x9e22c0, 0xc42031e600, 0xc4210c2b10, 0x22, 0x28, 0xa585d5496)
metrictank_1 | /usr/local/go/src/runtime/hashmap_fast.go:306 +0x52b fp=0xc4216a7f18 sp=0xc4216a7eb8
metrictank_1 | github.com/raintank/metrictank/mdata/cache.(*CCache).Add(0xc4202fa070, 0xc4210c2b10, 0x22, 0x0, 0xc421875f82, 0x25, 0x25, 0xa585d5496)
metrictank_1 | /home/mst/go/src/github.com/raintank/metrictank/mdata/cache/cache.go:77 +0x63 fp=0xc4216a7f80 sp=0xc4216a7f18
metrictank_1 | runtime.goexit()
metrictank_1 | /usr/local/go/src/runtime/asm_amd64.s:2086 +0x1 fp=0xc4216a7f88 sp=0xc4216a7f80
metrictank_1 | created by github.com/raintank/metrictank/api.(*Server).getSeries
metrictank_1 | /home/mst/go/src/github.com/raintank/metrictank/api/dataprocessor.go:442 +0x122b
UPDATE: I recompiled with -race and now I'm getting a different error. This looks as if the RWMutex were completely ineffective because according to the backtraces the problem must be in the combination of the evict and Add methods.
==================
WARNING: DATA RACE
Read at 0x00c4201c81e0 by goroutine 215:
runtime.mapaccess2_faststr()
/usr/local/go/src/runtime/hashmap_fast.go:297 +0x0
github.com/raintank/metrictank/mdata/cache.(*CCache).Add()
/home/mst/go/src/github.com/raintank/metrictank/mdata/cache/cache.go:77 +0xaa
Previous write at 0x00c4201c81e0 by goroutine 155:
runtime.mapdelete()
/usr/local/go/src/runtime/hashmap.go:558 +0x0
github.com/raintank/metrictank/mdata/cache.(*CCache).evict()
/home/mst/go/src/github.com/raintank/metrictank/mdata/cache/cache.go:97 +0x30e
github.com/raintank/metrictank/mdata/cache.(*CCache).evictLoop()
/home/mst/go/src/github.com/raintank/metrictank/mdata/cache/cache.go:70 +0xb3
Goroutine 215 (running) created at:
github.com/raintank/metrictank/api.(*Server).getSeries()
/home/mst/go/src/github.com/raintank/metrictank/api/dataprocessor.go:442 +0x17c9
github.com/raintank/metrictank/api.(*Server).getTarget()
/home/mst/go/src/github.com/raintank/metrictank/api/dataprocessor.go:331 +0x9c3
github.com/raintank/metrictank/api.(*Server).getTargetsLocal.func1()
/home/mst/go/src/github.com/raintank/metrictank/api/dataprocessor.go:284 +0xa9
Goroutine 155 (running) created at:
github.com/raintank/metrictank/mdata/cache.NewCCache()
/home/mst/go/src/github.com/raintank/metrictank/mdata/cache/cache.go:63 +0x12f
main.main()
/home/mst/go/src/github.com/raintank/metrictank/metrictank.go:388 +0x246c
==================

A colleague of mine has found the answer:
After calling NewCCache() I copied the returned variable by value (including the lock) and then called Add() on the copy, at the same time the evictLoop() go routine was still referring to the old copy. So they were operating on different copies of the lock :)

Related

Why a byte buffer from CGO can reads correctly but write fault?

I am using this library https://github.com/billziss-gh/cgofuse, and some interfaces need to be implemented,one of them looks like this:
func (self *Memfs) Write(path string, buff []byte, ofst int64, fh uint64) (n int) {
defer trace(path, buff, ofst, fh)(&n)
defer self.synchronize()()
node := self.getNode(path, fh)
if nil == node {
return -fuse.ENOENT
}
endofst := ofst + int64(len(buff))
if endofst > node.stat.Size {
node.data = resize(node.data, endofst, true)
node.stat.Size = endofst
}
fmt.Println("len(buff) = ", len(buff)) // (1)
fmt.Println("cap(buff) = ", cap(buff)) // (2)
fmt.Println("buff[0] = ", buff[0]) // (3)
buff[0] = 1 // (4)
n = copy(node.data[ofst:endofst], buff)
tmsp := fuse.Now()
node.stat.Ctim = tmsp
node.stat.Mtim = tmsp
return
}
This is a file system, Write is called when writing to a file. I added (1), (2), (3), (4) in the above code, but it was wrong at (4). The error stack is as follows:
unexpected fault address 0x116e6c60390
fatal error: fault
[signal 0xc0000005 code=0x1 addr=0x116e6c60390 pc=0xca7dad]
goroutine 17 [running, locked to thread]:
runtime.throw(0xcf373f, 0x5)
D:/Scoop/apps/go/current/src/runtime/panic.go:1117 +0x79 fp=0xc000033bc8 sp=0xc000033b98 pc=0xc18db9
runtime.sigpanic()
D:/Scoop/apps/go/current/src/runtime/signal_windows.go:245 +0x2d6 fp=0xc000033c20 sp=0xc000033bc8 pc=0xc2b7d6
main.(*Memfs).Write(0xc00001e400, 0xc00000a2b0, 0x9, 0x116e6c60390, 0x40000, 0x40000000, 0x0, 0x2, 0x9)
D:/code/go/LiangFs/tool/memfs.go:310 +0x4cd fp=0xc000033de0 sp=0xc000033c20 pc=0xca7dad
github.com/billziss-gh/cgofuse/fuse.hostWrite(0x116e00d1480, 0x116e6c60390, 0x40000, 0x0, 0x1518fff7c8, 0x0)
D:/go/pkg/mod/github.com/billziss-gh/cgofuse#v1.5.0/fuse/host.go:255 +0x102 fp=0xc000033e60 sp=0xc000033de0 pc=0xc9c282
github.com/billziss-gh/cgofuse/fuse.go_hostWrite(...)
D:/go/pkg/mod/github.com/billziss-gh/cgofuse#v1.5.0/fuse/host_cgo.go:911
_cgoexp_12ef5be0dd8c_go_hostWrite(0x1518fff710)
_cgo_gotypes.go:738 +0x59 fp=0xc000033ea0 sp=0xc000033e60 pc=0xca2919
runtime.cgocallbackg1(0xca28c0, 0x1518fff710, 0x0)
D:/Scoop/apps/go/current/src/runtime/cgocall.go:292 +0x19a fp=0xc000033f40 sp=0xc000033ea0 pc=0xbe4c5a
runtime.cgocallbackg(0xca28c0, 0x1518fff710, 0x0)
D:/Scoop/apps/go/current/src/runtime/cgocall.go:228 +0xfc fp=0xc000033fb8 sp=0xc000033f40 pc=0xbe49bc
runtime.cgocallback(0x0, 0x0, 0x0)
D:/Scoop/apps/go/current/src/runtime/asm_amd64.s:788 +0xc0 fp=0xc000033fe0 sp=0xc000033fb8 pc=0xc48bc0
runtime.goexit()
D:/Scoop/apps/go/current/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc000033fe8 sp=0xc000033fe0 pc=0xc48ea1
goroutine 1 [syscall]:
github.com/billziss-gh/cgofuse/fuse._Cfunc_hostMount(0x3, 0xc00001e420, 0x116e0316540, 0x0)
_cgo_gotypes.go:502 +0x4f
github.com/billziss-gh/cgofuse/fuse.c_hostMount.func1(0xc000000003, 0xc00001e420, 0x116e0316540, 0x116e0316540)
D:/go/pkg/mod/github.com/billziss-gh/cgofuse#v1.5.0/fuse/host_cgo.go:820 +0x8c
github.com/billziss-gh/cgofuse/fuse.c_hostMount(0xc000000003, 0xc00001e420, 0x116e0316540, 0xb)
D:/go/pkg/mod/github.com/billziss-gh/cgofuse#v1.5.0/fuse/host_cgo.go:820 +0x45
github.com/billziss-gh/cgofuse/fuse.(*FileSystemHost).Mount(0xc00003e040, 0xcf4632, 0xb, 0xc000034210, 0x0, 0x0, 0x1ffffff00)
D:/go/pkg/mod/github.com/billziss-gh/cgofuse#v1.5.0/fuse/host.go:704 +0x413
main.main()
D:/code/go/LiangFs/tool/memfs.go:594 +0xce
len(buff) = 262144
cap(buff) = 1073741824
buff[0] = 97
the content of buff is all 97, becasue I copy the following file to this file system:
The code comes from the example in the library https://github.com/billziss-gh/cgofuse/blob/master/examples/memfs/memfs.go , I just added the (1), (2), (3), (4) mentioned above.
My os is windows 10, go version is go1.16.7 windows/amd64.
Why does assigning a slice element make a mistake? Is it because the library uses CGO?
I am the author of both WinFsp and cgofuse and can explain what is happening here.
The buffer that you receive in Write should always be treated as a read-only buffer. Attempting to write into this buffer may result in an access violation. This is by design.
When an application issues a WriteFile request, the WinFsp kernel mode driver has to somehow transfer the data from the application to the user mode file system. The driver has a few different strategies for doing so and under certain circumstances it will choose a zero-copy technique: it will map the application buffer directly into the address space of the user mode file system, thus avoiding an expensive memory copy.
In the case of Write this zero-copy mapping will always be read-only in order to avoid the case where a user mode accidentally or maliciously writes into an application's WriteFile buffer. (Also note that when the WinFsp driver sets up this mapping, it does so in a way that ensures that there is no accidental data leakage from the application into the user mode file system either.)
Since nothing is printed after buf[0] = 1 // (4), we don't know wether the panic comes from this line of code or a line afterwards ; you have to debug further to spot where the bug lies.
Actually : the stack trace you pasted mentions LiangFs/tool/memfs.go:310.
If this line number refers to the file you linked to, this line is the return instruction in your .Write() function.
The error must be triggered by one of the two defer calls (which get executed when you return from your function).

how to get Go Delve debugger (dlv) 'display' command to show all values of a slice or map

I'm trying to use the Delve (dlv) "display" command to show the values of a slice and a map. The "print" command shows the full value but "display" only ever shows "[...]"
contrast the display and print output below
(dlv) display
0: gns = []string len: 2, cap: 2, [...]
1: chGnMap = map[string]int [...]
(dlv) p gns
[]string len: 2, cap: 2, ["ecam","site"]
(dlv) p chGnMap
map[string]int [
"ecam": 2,
"site": 2,
]
(dlv) config -list
aliases map[]
substitute-path []
max-string-len 1024
max-array-values 1024
max-variable-recurse 10
disassemble-flavor <not defined>
show-location-expr false
source-list-line-color <nil>
source-list-arrow-color ""
source-list-keyword-color ""
source-list-string-color ""
source-list-number-color ""
source-list-comment-color ""
source-list-line-count <not defined>
debug-info-directories [/usr/lib/debug/.build-id]
(dlv) exit
# dlv version
Delve Debugger
Version: 1.7.2
This doesn't entirely answer your question, but:
When you are adding your display variables display -a ..., you can reference a key in the dictionary.
See steps below:
Add map w/ key supplied using display -a
Show that the key currently doesn't exist
The key is automatically added when the program advances
Note: I needed to append [0] to the display line because
handlerHeader["Content-Type"] returns a string slice.
(dlv) args
handler = (*main.ProduceHandler)(0x14000112d10)
wri = net/http.ResponseWriter(*net/http.response) 0x14000193708
req = ("*net/http.Request")(0x14000182000)
(dlv) display -a wri.w.wr.res.handlerHeader["Content-Type"][0]
0: wri.w.wr.res.handlerHeader["Content-Type"][0] = error key not found
(dlv) print %T wri.w.wr.res.handlerHeader
net/http.Header []
(dlv) n
> main.(*ProduceHandler).ServeHTTP() ./api.go:144 (PC: 0x100984480)
139: switch req.Method {
140: case http.MethodGet:
141: if len(req.URL.Query()["code"]) == 0 {
142: log.Println("Sending entire produce database")
143: wri.Header().Add("Content-Type", "application/json")
=> 144: wri.WriteHeader(http.StatusOK)
145: json.NewEncoder(wri).Encode(handler.DB)
146: return
147: }
148:
149: c := req.URL.Query()["code"][0]
0: wri.w.wr.res.handlerHeader["Content-Type"][0] = "application/json"

Linux kernel: Will wait_event_interruptible sleep forever when the preemption happens right after prepare_to_wait_event? [duplicate]

This question already has an answer here:
Will process lost wake-up chance in a preemptive kernel?
(1 answer)
Closed 1 year ago.
I'm a bit confused when reading the kernel's source code on wait_event_interruptible because it does not look safe when taking into account the preemption.
Here is the main logic of the source code:
1: #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
2: ({ \
3: __label__ __out; \
4: struct wait_queue_entry __wq_entry; \
5: long __ret = ret; /* explicit shadow */ \
6: \
7: init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
8: for (;;) { \
9: long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
10: \
11: if (condition) \
12: break; \
13: \
14: if (___wait_is_interruptible(state) && __int) { \
15: __ret = __int; \
16: goto __out; \
17: } \
18: \
19: cmd; \
20: } \
21: finish_wait(&wq_head, &__wq_entry); \
22: __out: __ret; \
23: })
This macro is not wrapped in a preemption-disabling block, and so I think it's possible that the corresponding process could be switched out in line 10. So under the following scenario:
There is only on wakeup for this sleep.
The aforementioned wakeup occurs in another cpu when this code is in line 8.
An interrupt kicked in at line 10, and this process is switched out as the result.
As a result, this process will stay in TASK_INTERRUPTIBLE forever.
I'm wondering if that is the case or did I miss anything?
The linux code I'm looking at: https://elixir.bootlin.com/linux/v5.13.4/source/include/linux/wait.h#L274
Surely, it would stay hang but next time when process gets schedule and it resumes it would check the condition and would find condition as true and it would break.
Let suppose condition goes bad as other process modified value then it would hang until someone trigger signal to it and this process would check the signal and break out of for loop.

sbcl memory-fault-error with zeromq

I am new to Lisp, and am trying to work with ZMQ in it. For a simple function, I get the error below. How do I debug this error? Are there tools to help? It isn't dropping me into the ldb, how do I see what is at the address printed with the error?
Unhandled memory fault at #x10CC8B000.
[Condition of type SB-SYS:MEMORY-FAULT-ERROR]
Restarts:
0: [RETRY] Retry SLIME REPL evaluation request.
1: [*ABORT] Return to SLIME's top level.
2: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {1003E0F353}>)
Backtrace:
0: (SB-SYS:MEMORY-FAULT-ERROR)
[No Locals]
1: ("foreign function: call_into_lisp")
[No Locals]
2: ("foreign function: post_signal_tramp")
[No Locals]
3: ("foreign function: _ZN3zmq6pipe_t12get_identityEv")
[No Locals]
4: ("foreign function: _ZN3zmq8router_t5xrecvEPNS_5msg_tE")
[No Locals]
5: ("foreign function: _ZN3zmq5rep_t5xrecvEPNS_5msg_tE")
[No Locals]
6: ("foreign function: _ZN3zmq13socket_base_t4recvEPNS_5msg_tEi")
[No Locals]
7: ("foreign function: _ZL9s_recvmsgPN3zmq13socket_base_tEP9zmq_msg_ti")
[No Locals]
8: (ZEROMQ::%MSG-RECV #.(SB-SYS:INT-SAP #X005002D0) #.(SB-SYS:INT-SAP #X06006000) 0)
Locals:
SB-DEBUG::ARG-0 = #.(SB-SYS:INT-SAP #X005002D0)
SB-DEBUG::ARG-1 = #.(SB-SYS:INT-SAP #X06006000)
SB-DEBUG::ARG-2 = 0
9: ((LAMBDA (&REST SB-DI::ARGS) :IN SB-DI::HANDLE-SINGLE-STEP-AROUND-TRAP) #.(SB-SYS:INT-SAP #X06006000) #S(ZEROMQ:MSG :RAW #.(SB-SYS:INT-SAP #X005002D0)))
Locals:
SB-DI::ARGS = (#.(SB-SYS:INT-SAP #X06006000) #S(ZEROMQ:MSG :RAW #.(SB-SYS:INT-SAP #X005002D0)))
10: (RECV)
Locals:
CONTEXT = #.(SB-SYS:INT-SAP #X00500CC0)
SOCKET = #.(SB-SYS:INT-SAP #X06006000)
11: (SB-INT:SIMPLE-EVAL-IN-LEXENV (RECV) #<NULL-LEXENV>)
Locals:
SB-DEBUG::ARG-0 = (RECV)
SB-DEBUG::ARG-1 = #<NULL-LEXENV>
12: (EVAL (RECV))
Locals:
SB-DEBUG::ARG-0 = (RECV)
--more--
Here is the code I am trying to run. I am running these two functions in two different sbcl processes
sbcl<1>
(defun send ()
(zmq:with-context (context)
(zmq:with-socket (socket context :req)
(break)
(zmq:connect socket "tcp://127.0.0.1:3333")
(break)
(zmq:msg-send socket (zmq:make-msg :size 6 :data "Jello")))))
(send)
sbcl<2>
(defun recv ()
(zmq:with-context (context)
(zmq:with-socket (socket context :rep)
(break)
(zmq:bind socket "tcp://127.0.0.1:3333")
(break)
(zmq:msg-recv
socket (zmq:make-msg :size 6)))))
(recv)
[EDIT]
After a bit of digging around, I found this message in the buffer
* sbcl(49664,0x700000104000) malloc: *** error for object 0x500058: incorrect checksum for freed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
CORRUPTION WARNING in SBCL pid 49664(tid 123145303375872):
Received signal 6 in non-lisp thread 123145303375872, resignalling to a lisp thread.
The integrity of this image is possibly compromised.
Continuing with fingers crossed.
I have used the same C library with it's Python & Node bindings and it works without errors in Python and Node. I am only running into these errors with CL
So after a few days of hanging out in the #zeromq IRC, using lldb to debug the memory issues, I finally gave up. The CL bindings for zeromq aren't tested on OS X, and they do not work.
I switched to Ubuntu, and had no such problems.

Efficient string concatenation in Scala

The JVM optimzes String concatenation with + and replaces it with a StringBuilder. This should be the same in Scala. But what happens if strings are concatenated with ++=?
var x = "x"
x ++= "y"
x ++= "z"
As far as I know this methods treats strings like char seqences, so even if the JVM would create a StringBuilder it would lead to many method calls, right? Would it be better to use a StringBuilder instead?
To what type is the String converted implicitly?
There is a huge HUGE difference in time taken.
If you add strings repeatedly using += you do not optimize away the O(n^2) cost of creating incrementally longer strings. So for adding one or two you won't see a difference, but it doesn't scale; by the time you get to adding 100 (short) strings, using a StringBuilder is over 20x faster. (Precise data: 1.3 us vs. 27.1 us to add the string representations of the numbers 0 to 100; timings should be reproducible to about += 5% and of course are for my machine.)
Using ++= on a var String is far far worse yet, because you are then instructing Scala to treat a string as a character-by-character collection which then requires all sorts of wrappers to make the String look like a collection (including boxed character-by-character addition using the generic version of ++!). Now you're 16x slower again on 100 additions! (Precise data: 428.8 us for ++= on a var string instead of +='s 26.7 us.)
If you write a single statement with a bunch of +es then the Scala compiler will use a StringBuilder and end up with an efficient result (Data: 1.8 us on non-constant strings pulled out of an array).
So, if you add strings with anything other than + in line, and you care about efficiency, use a StringBuilder. Definitely don't use ++= to add another String to a var String; there just isn't any reason to do it, and there's a big runtime penalty.
(Note--very often you don't care at all how efficient your string additions are! Don't clutter your code with extra StringBuilders unless you have reason to suspect that this particular code path is getting called a lot.)
Actually, the inconvenient truth is StringOps usually remains an allocation:
scala> :pa
// Entering paste mode (ctrl-D to finish)
class Concat {
var x = "x"
x ++= "y"
x ++= "z"
}
// Exiting paste mode, now interpreting.
defined class Concat
scala> :javap -prv Concat
Binary file Concat contains $line3.$read$$iw$$iw$Concat
Size 1211 bytes
MD5 checksum 1900522728cbb0ed0b1d3f8b962667ad
Compiled from "<console>"
public class $line3.$read$$iw$$iw$Concat
SourceFile: "<console>"
[snip]
public $line3.$read$$iw$$iw$Concat();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=6, locals=1, args_size=1
0: aload_0
1: invokespecial #19 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #20 // String x
7: putfield #10 // Field x:Ljava/lang/String;
10: aload_0
11: new #22 // class scala/collection/immutable/StringOps
14: dup
15: getstatic #28 // Field scala/Predef$.MODULE$:Lscala/Predef$;
18: aload_0
19: invokevirtual #30 // Method x:()Ljava/lang/String;
22: invokevirtual #34 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
25: invokespecial #36 // Method scala/collection/immutable/StringOps."<init>":(Ljava/lang/String;)V
28: new #22 // class scala/collection/immutable/StringOps
31: dup
32: getstatic #28 // Field scala/Predef$.MODULE$:Lscala/Predef$;
35: ldc #38 // String y
37: invokevirtual #34 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
40: invokespecial #36 // Method scala/collection/immutable/StringOps."<init>":(Ljava/lang/String;)V
43: getstatic #28 // Field scala/Predef$.MODULE$:Lscala/Predef$;
46: invokevirtual #42 // Method scala/Predef$.StringCanBuildFrom:()Lscala/collection/generic/CanBuildFrom;
49: invokevirtual #46 // Method scala/collection/immutable/StringOps.$plus$plus:(Lscala/collection/GenTraversableOnce;Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;
52: checkcast #48 // class java/lang/String
55: invokevirtual #50 // Method x_$eq:(Ljava/lang/String;)V
See more demonstration at this answer.
Edit: To say more, you're building the String on each reassignment, so, no you're not using a single StringBuilder.
However, the optimization is done by javac and not the JIT compiler, so to compare fruits of the same kind:
public class Strcat {
public String strcat(String s) {
String t = " hi ";
String u = " by ";
return s + t + u; // OK
}
public String strcat2(String s) {
String t = s + " hi ";
String u = t + " by ";
return u; // bad
}
}
whereas
$ scala
Welcome to Scala version 2.11.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_11).
Type in expressions to have them evaluated.
Type :help for more information.
scala> :se -Xprint:typer
scala> class K { def f(s: String, t: String, u: String) = s ++ t ++ u }
[[syntax trees at end of typer]] // <console>
def f(s: String, t: String, u: String): String = scala.this.Predef.augmentString(scala.this.Predef.augmentString(s).++[Char, String](scala.this.Predef.augmentString(t))(scala.this.Predef.StringCanBuildFrom)).++[Char, String](scala.this.Predef.augmentString(u))(scala.this.Predef.StringCanBuildFrom)
is bad. Or, worse, to unroll Rex's explanation:
"abc" ++ "def"
augmentString("abc").++[Char, String](augmentString("def"))(StringCanBuildFrom)
collection.mutable.StringBuilder.newBuilder ++= new WrappedString(augmentString("def"))
val b = collection.mutable.StringBuilder.newBuilder
new WrappedString(augmentString("def")) foreach b.+=
As Rex explained, StringBuilder overrides ++=(String) but not Growable.++=(Traversable[Char]).
In case you've ever wondered what unaugmentString is for:
28: invokevirtual #40 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
31: invokevirtual #43 // Method scala/Predef$.unaugmentString:(Ljava/lang/String;)Ljava/lang/String;
34: invokespecial #46 // Method scala/collection/immutable/WrappedString."<init>":(Ljava/lang/String;)V
And just to show that you do finally call unadorned +=(Char) but after boxing and unboxing:
public final scala.collection.mutable.StringBuilder apply(char);
flags: ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: getfield #19 // Field b$1:Lscala/collection/mutable/StringBuilder;
4: iload_1
5: invokevirtual #24 // Method scala/collection/mutable/StringBuilder.$plus$eq:(C)Lscala/collection/mutable/StringBuilder;
8: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this L$line10/$read$$iw$$iw$$anonfun$1;
0 9 1 x C
LineNumberTable:
line 9: 0
public final java.lang.Object apply(java.lang.Object);
flags: ACC_PUBLIC, ACC_FINAL, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokestatic #35 // Method scala/runtime/BoxesRunTime.unboxToChar:(Ljava/lang/Object;)C
5: invokevirtual #37 // Method apply:(C)Lscala/collection/mutable/StringBuilder;
8: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this L$line10/$read$$iw$$iw$$anonfun$1;
0 9 1 v1 Ljava/lang/Object;
LineNumberTable:
line 9: 0
A good laugh does get some oxygen into the bloodstream.

Resources