7

Which is the quickest, and most efficient way to concatenate multiple strings in Swift 2?

// Solution 1...
let newString:String = string1 + " " + string2
// ... Or Solution 2?
let newString:String = "\(string1) \(string2)"

Or is the only differentiation the way it looks to the programmer?

James
  • 1,088
  • 3
  • 11
  • 29
  • 1
    There is also the String function appendContentsOf, which only appends 1 string, but would be good to benchmark as well. – will Jan 30 '16 at 23:31

5 Answers5

8

I ran the following code in the simulator and on an iPhone6S Plus. The results in both cases showed the string1 + " " + string2 addition faster for the strings I used. I didn't try with different types of strings, optimizations, etc. but you can run the code and check for your particular strings etc. Try running this code online in the IBM Swift Sandbox. The timer struct is from here: Measure elapsed time in Swift

To run the code create a single view application in Xcode and add the following code to the ViewController:

import UIKit
import CoreFoundation

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let a = "abscdefghi jkl¢€@sads dljlæejktæljæ leijroptjiæa Dog! iojeg r æioej rgæoija"
        let b = a
        timeStringAdding(a, string2: b, times: 1_000_000, repetitions: 5)
    }
            
    struct RunTimer: CustomStringConvertible {
        var begin: CFAbsoluteTime
        var end:CFAbsoluteTime
        
        init() {
            begin = CFAbsoluteTimeGetCurrent()
            end = 0
        }

        mutating func start() {
            begin = CFAbsoluteTimeGetCurrent()
            end = 0
        }

        @discardableResult
        mutating func stop() -> Double {
            if (end == 0) { end = CFAbsoluteTimeGetCurrent() }
            return Double(end - begin)
        }

        var duration: CFAbsoluteTime {
            get {
                if (end == 0) { return CFAbsoluteTimeGetCurrent() - begin }
                else { return end - begin }
            }
        }

        var description: String {
            let time = duration
            if (time > 100) {return " \(time/60) min"}
            else if (time < 1e-6) {return " \(time*1e9) ns"}
            else if (time < 1e-3) {return " \(time*1e6) µs"}
            else if (time < 1) {return " \(time*1000) ms"}
            else {return " \(time) s"}
        }
    }
    
    func timeStringAdding(string1:String, string2:String, times:Int, repetitions:Int) {
        var newString = ""
        var i = 0
        var j = 0
        var timer = RunTimer()
        
        while j < repetitions {
            i = 0
            timer.start()
            while i < times {
                newString = string1 + " " + string2
                i += 1
            }
            print("+ add \(timer)")
            
            i = 0
            timer.start()
            while i < times {
                newString = "\(string1) \(string2)"
                i += 1
            }
            print("\\(  add \(timer)")
            j += 1
        }
    }
}

On an iPhone 6S Plus, it gave:

+   add  727.977991104126 ms
\(  add  1.1197350025177 s

+   add  693.499982357025 ms
\(  add  1.11982899904251 s

+   add  690.113961696625 ms
\(  add  1.12086200714111 s

+   add  707.363963127136 ms
\(  add  1.13451600074768 s

+   add  734.095990657806 ms
\(  add  1.19673496484756 s

And on the simulator (iMac Retina):

+   add  406.143009662628 ms
\(  add  594.823002815247 ms

+   add  366.503953933716 ms
\(  add  595.698952674866 ms

+   add  370.530009269714 ms
\(  add  596.457958221436 ms

+   add  369.667053222656 ms
\(  add  594.724953174591 ms

+   add  369.095981121063 ms
\(  add  595.37798166275 ms

Most of the time is allocation and freeing memory for the string structs and for those really curious run the code in the Instruments panel with Time Profiler usage and see how the time is allocated for alloc and free etc, in relation to the machine code which is shown there also.

Sverrisson
  • 17,970
  • 5
  • 66
  • 62
  • 1
    Hm, this is the worst benchmark I've ever seen. It depends on many other factors other than the actual operation. I suggest using `/usr/bin/time` command with path to compiled executable file as argument instead. – tie Jan 31 '16 at 12:44
  • @b1nary yes if you want the user time, but I would rather run the code in Instruments panel with CPU usage and see how the time is allocated for alloc and free etc. But in this case a quick and dirty solution gives an idea if the difference is an order of magnitude or not. – Sverrisson Jan 31 '16 at 14:50
  • Running benchmarks in the Sandbox is kind of tricky right now, since jobs aren't allowed to run for longer than five seconds. It's a great way to quickly share code, but if you're running a long benchmark test, it's probably best to copy the Sandbox code into Xcode. – TheSoundDefense Feb 01 '16 at 17:06
  • 1
    @TheSoundDefense Thanks, but I ran the code on iPhone and Mac as listed in the answer. The Sandbox link is there to give a fun option to play with the code, as you mention, and I adjusted it so it wont run over the 5s limit. But, users can test other methods, etc. And I am trying to advertise this great effort by IBM! – Sverrisson Feb 01 '16 at 17:14
2

This question piqued my curiosity, so I put this into a new project. These are quick and dirty benchmarks and should be taken with the usual grains of salt, but the results were intriguing.

var string1 = "This"
var string2 = "that"
var newString: String

let startTime = NSDate()
for _ in 1...100000000 {
  newString = string1 + " " + string2
}
print("Diff: \(startTime.timeIntervalSinceNow * -1)")

In 6 runs on the simulator running on my MacBook Pro (mid-2014 i7, 2.5GHz), the output to the debug console averaged 1.36 seconds. Deployed as debug code to my iPhone 6S, the average of all the outputs in 6 runs was 1.33 seconds.

Using the same code but changing the line where the strings are concatenated to this...

newString = "\(string1) \(string2)"

...gave me rather different results. In 6 runs on the simulator, the average time reported on the debug console was 50.86 seconds. In 6 runs on the iPhone 6S, the average run time was 88.82 seconds. That's almost 2 orders of magnitude's difference.

These results suggest that if you have to concatenate a large number of strings, you should use the + operator rather than string interpolation.

Joey deVilla
  • 8,403
  • 2
  • 29
  • 15
1

The quickest way is likely implementation dependent. You could benchmark your particular combination of compiler, compiler optimization settings, library, framework, OS, and processor/CPU. But it's likely the performance might be very different under a different combination.

The answer might also differ between whether string1 and string2 are mutable or not, but also depending on compiler optimization level.

hotpaw2
  • 70,107
  • 14
  • 90
  • 153
  • You could also do something like: xcrun -sdk macosx swiftc -emit-assembly mytestcode.swift to see which spits out less asm code. – hotpaw2 Jan 30 '16 at 22:01
1

Swift 3

Another way to concatenate strings in Swift 3 is to use joined:

let stringArray = ["string1", "string2"]
let newString = stringArray.joined(separator: " ")

Of course, this requires that the strings are in an array. I have not done any time profile so cannot compare it to the other suggested solutions.

koen
  • 5,383
  • 7
  • 50
  • 89
0

TL;DR: The difference is noticeable only if you are working with a big strings (millions of bytes/characters).

All tests were compiled and executed on iMac 21.5 Late 2012, 2.7GHz Intel Core i5.

I've made a small benchmark. Here is the code.

interpolation.swift compiled with swiftc ./interpolation.swift -o ./interpolation

import Swift

_ = "\(Process.arguments[1]) \(Process.arguments[2])"

Output of swiftc with -emit-assembly flag:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 9
    .globl  _main
    .align  4, 0x90
_main:
    .cfi_startproc
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    subq    $128, %rsp
    movq    _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token5@GOTPCREL(%rip), %rax
    movq    __TZvOs7Process5_argcVs5Int32@GOTPCREL(%rip), %rcx
    movl    %edi, (%rcx)
    cmpq    $-1, (%rax)
    movq    %rsi, -56(%rbp)
    je  LBB0_2
    movq    _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token5@GOTPCREL(%rip), %rdi
    movq    _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func5@GOTPCREL(%rip), %rax
    movq    %rax, %rsi
    callq   _swift_once
LBB0_2:
    movl    $5, %eax
    movl    %eax, %edi
    movq    __TZvOs7Process11_unsafeArgvGSpGSpVs4Int8__@GOTPCREL(%rip), %rcx
    movq    -56(%rbp), %rdx
    movq    %rdx, (%rcx)
    callq   __TTSg5SS___TFs27_allocateUninitializedArrayurFBwTGSax_Bp_
    leaq    L___unnamed_1(%rip), %rdi
    xorl    %esi, %esi
    movl    $1, %r8d
    movq    %rdx, -64(%rbp)
    movl    %r8d, %edx
    movq    %rax, -72(%rbp)
    callq   __TFSSCfT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS
    movq    %rax, %rdi
    movq    %rdx, %rsi
    movq    %rcx, %rdx
    callq   __TFSSCfT26stringInterpolationSegmentSS_SS
    movq    -64(%rbp), %rsi
    movq    %rax, (%rsi)
    movq    %rdx, 8(%rsi)
    movq    %rcx, 16(%rsi)
    callq   __TFOs7Processau9argumentsGSaSS_
    movq    (%rax), %rax
    movq    %rax, %rdi
    movq    %rax, -80(%rbp)
    callq   _swift_bridgeObjectRetain
    leaq    -24(%rbp), %rdi
    movl    $1, %r8d
    movl    %r8d, %esi
    movq    -80(%rbp), %rdx
    movq    %rax, -88(%rbp)
    callq   __TTSg5SS___TFSag9subscriptFSix
    movq    -80(%rbp), %rdi
    callq   _swift_bridgeObjectRelease
    movq    -24(%rbp), %rdi
    movq    -16(%rbp), %rsi
    movq    -8(%rbp), %rdx
    callq   __TFSSCfT26stringInterpolationSegmentSS_SS
    leaq    L___unnamed_2(%rip), %rdi
    movl    $1, %r8d
    movl    %r8d, %esi
    movl    $1, %r8d
    movq    -64(%rbp), %r9
    movq    %rax, 24(%r9)
    movq    %rdx, 32(%r9)
    movq    %rcx, 40(%r9)
    movl    %r8d, %edx
    callq   __TFSSCfT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS
    movq    %rax, %rdi
    movq    %rdx, %rsi
    movq    %rcx, %rdx
    callq   __TFSSCfT26stringInterpolationSegmentSS_SS
    movq    -64(%rbp), %rsi
    movq    %rax, 48(%rsi)
    movq    %rdx, 56(%rsi)
    movq    %rcx, 64(%rsi)
    callq   __TFOs7Processau9argumentsGSaSS_
    movq    (%rax), %rax
    movq    %rax, %rdi
    movq    %rax, -96(%rbp)
    callq   _swift_bridgeObjectRetain
    leaq    -48(%rbp), %rdi
    movl    $2, %r8d
    movl    %r8d, %esi
    movq    -96(%rbp), %rdx
    movq    %rax, -104(%rbp)
    callq   __TTSg5SS___TFSag9subscriptFSix
    movq    -96(%rbp), %rdi
    callq   _swift_bridgeObjectRelease
    movq    -48(%rbp), %rdi
    movq    -40(%rbp), %rsi
    movq    -32(%rbp), %rdx
    callq   __TFSSCfT26stringInterpolationSegmentSS_SS
    leaq    L___unnamed_1(%rip), %rdi
    xorl    %r8d, %r8d
    movl    %r8d, %esi
    movl    $1, %r8d
    movq    -64(%rbp), %r9
    movq    %rax, 72(%r9)
    movq    %rdx, 80(%r9)
    movq    %rcx, 88(%r9)
    movl    %r8d, %edx
    callq   __TFSSCfT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS
    movq    %rax, %rdi
    movq    %rdx, %rsi
    movq    %rcx, %rdx
    callq   __TFSSCfT26stringInterpolationSegmentSS_SS
    movq    -64(%rbp), %rsi
    movq    %rax, 96(%rsi)
    movq    %rdx, 104(%rsi)
    movq    %rcx, 112(%rsi)
    movq    -72(%rbp), %rdi
    callq   __TFSSCft19stringInterpolationGSaSS__SS
    movq    %rcx, %rdi
    movq    %rax, -112(%rbp)
    movq    %rdx, -120(%rbp)
    callq   _swift_unknownRelease
    xorl    %eax, %eax
    addq    $128, %rsp
    popq    %rbp
    retq
    .cfi_endproc

    .section    __TEXT,__cstring,cstring_literals
L___unnamed_1:
    .space  1

L___unnamed_2:
    .asciz  " "

    .linker_option "-lswiftCore"
    .linker_option "-lobjc"
    .section    __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
    .long   0
    .long   768


.subsections_via_symbols

addstr.swift (+ operator) compiled with swiftc ./addstr.swift -o ./addstr

import Swift

_ = Process.arguments[1] + " " + Process.arguments[2]

Output of swiftc with -emit-assembly flag:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 9
    .globl  _main
    .align  4, 0x90
_main:
    .cfi_startproc
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    subq    $176, %rsp
    movq    _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token5@GOTPCREL(%rip), %rax
    movq    __TZvOs7Process5_argcVs5Int32@GOTPCREL(%rip), %rcx
    movl    %edi, (%rcx)
    cmpq    $-1, (%rax)
    movq    %rsi, -56(%rbp)
    je  LBB0_2
    movq    _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_token5@GOTPCREL(%rip), %rdi
    movq    _globalinit_33_1BDF70FFC18749BAB495A73B459ED2F0_func5@GOTPCREL(%rip), %rax
    movq    %rax, %rsi
    callq   _swift_once
LBB0_2:
    movq    __TZvOs7Process11_unsafeArgvGSpGSpVs4Int8__@GOTPCREL(%rip), %rax
    movq    -56(%rbp), %rcx
    movq    %rcx, (%rax)
    callq   __TFOs7Processau9argumentsGSaSS_
    movq    (%rax), %rax
    movq    %rax, %rdi
    movq    %rax, -64(%rbp)
    callq   _swift_bridgeObjectRetain
    leaq    -24(%rbp), %rdi
    movl    $1, %edx
    movl    %edx, %esi
    movq    -64(%rbp), %rdx
    movq    %rax, -72(%rbp)
    callq   __TTSg5SS___TFSag9subscriptFSix
    movq    -64(%rbp), %rdi
    callq   _swift_bridgeObjectRelease
    leaq    L___unnamed_1(%rip), %rdi
    movl    $1, %r8d
    movl    %r8d, %esi
    movl    $1, %edx
    movq    -24(%rbp), %rax
    movq    -16(%rbp), %rcx
    movq    -8(%rbp), %r9
    movq    %r9, -80(%rbp)
    movq    %rcx, -88(%rbp)
    movq    %rax, -96(%rbp)
    callq   __TFSSCfT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1__SS
    movq    -96(%rbp), %rdi
    movq    -88(%rbp), %rsi
    movq    -80(%rbp), %r9
    movq    %rdx, -104(%rbp)
    movq    %r9, %rdx
    movq    %rcx, -112(%rbp)
    movq    %rax, %rcx
    movq    -104(%rbp), %r8
    movq    -112(%rbp), %r9
    callq   __TZFsoi1pFTSSSS_SS
    movq    %rax, -120(%rbp)
    movq    %rdx, -128(%rbp)
    movq    %rcx, -136(%rbp)
    callq   __TFOs7Processau9argumentsGSaSS_
    movq    (%rax), %rax
    movq    %rax, %rdi
    movq    %rax, -144(%rbp)
    callq   _swift_bridgeObjectRetain
    leaq    -48(%rbp), %rdi
    movl    $2, %r10d
    movl    %r10d, %esi
    movq    -144(%rbp), %rdx
    movq    %rax, -152(%rbp)
    callq   __TTSg5SS___TFSag9subscriptFSix
    movq    -144(%rbp), %rdi
    callq   _swift_bridgeObjectRelease
    movq    -48(%rbp), %rcx
    movq    -40(%rbp), %r8
    movq    -32(%rbp), %r9
    movq    -120(%rbp), %rdi
    movq    -128(%rbp), %rsi
    movq    -136(%rbp), %rdx
    callq   __TZFsoi1pFTSSSS_SS
    movq    %rcx, %rdi
    movq    %rax, -160(%rbp)
    movq    %rdx, -168(%rbp)
    callq   _swift_unknownRelease
    xorl    %eax, %eax
    addq    $176, %rsp
    popq    %rbp
    retq
    .cfi_endproc

    .section    __TEXT,__cstring,cstring_literals
L___unnamed_1:
    .asciz  " "

    .linker_option "-lswiftCore"
    .linker_option "-lobjc"
    .section    __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
    .long   0
    .long   768


.subsections_via_symbols

As you can see, the assembly of addstr.swift contains less commands than interpolation.swift.

Here are the benchmark results using /usr/bin/time for timing (bash-3.2).

$ ARG1=$(printf '%.0s' {1..30000}) # 30000 '' characters
$ ARG2=$(printf '%.0s' {1..30000}) # 30000 '' characters

$ time ./interpolation $ARG1 $ARG2
> 
> real  0m0.026s
> user  0m0.018s
> sys   0m0.006s

$ time ./addstr $ARG1 $ARG2
> 
> real  0m0.026s
> user  0m0.018s
> sys   0m0.006s

I've run this test lots of times, but the results were always the same (±0.001s).

tie
  • 693
  • 7
  • 13
  • This is not measuring the string addition. You can change one of your codes to add the strings two times, but you would still get the same results. Your actually measuring the time it takes to start the program, thus the results are the same because the string additions take less than a milli second. – Sverrisson Jan 31 '16 at 16:17
  • @HannesSverrisson have a look at this [Gist](https://gist.github.com/b1narykid/3f1a6f61b855482223ce) (it uses Swift's GYB util for code generation). It has *60_000_000* characters in the input string and runs the addition operation 1000 times by default. – tie Jan 31 '16 at 18:12
  • but now your measuring the allocation time by allocating strings with 60 million characters. Use your strings from your code above and run the operations million times and give us the results. – Sverrisson Jan 31 '16 at 19:33