str = str + "abc"比str = "abc" + str慢吗?

3

你能相信吗? 我有一个像这样的循环(请原谅任何错误,我不得不大量删除了许多信息和变量名,请相信它是有效的)。

...旧示例已编辑,见下面的代码...

如果我将那些中间的str = "Blah \(odat.count)" + str类型的行更改为str = str + "Blah \(odat.count)",用户界面就会停止响应,我得到一个彩色的圆形指针。NSTextField确实会到达第一个self.display.string...,但然后就会冻结。

由于我不熟悉多线程编程,所以请随意纠正我的方法。希望我想要实现的内容是清楚的。

我必须承认,工作版本也有点卡顿,但从来没有真正的死机过。 典型值为n=70,var3=7。

编辑:

这里是一个完全可用的示例。只需连接文本视图、进度条和按钮即可。尝试在主函数之间进行切换。

//
//  Controllers.swift
//
//

import Cocoa

class MainController: NSObject {

    @IBOutlet var display: NSTextView!
    @IBOutlet weak var prog: NSProgressIndicator!

    @IBAction func go1(sender: AnyObject) {
        theRoutine(70)
    }

    @IBAction func go2(sender: AnyObject) {
        theRoutine(50)
    }

    class SomeClass {
        var x: Int
        var y: Int
        var p: Double

        init?(size: Int, pro: Double) {
            x = size
            y = size
            p = pro
        }
    }

    func theRoutine(n: Int) {
        prog.hidden = false
        prog.doubleValue = 0
        prog.maxValue = 7 * 40
        let priority = DISPATCH_QUEUE_PRIORITY_HIGH
        dispatch_async(dispatch_get_global_queue(priority, 0)) {
            self.theFunc(n, var1: 0.06, var2: 0.06, var3: 7)
            self.theFunc(n, var1: 0.1*log(Double(n))/Double(n), var2: 0.3*log(Double(n))/Double(n), var3: 7)
            dispatch_async(dispatch_get_main_queue()) {
                self.prog.hidden = true
                self.appOut("done!")
            }
        }
    }

    //This doesn't
//  func theFunc(n: Int, var1: Double, var2: Double, var3: Int) {
//      var m: AnEnum
//      var gra: SomeClass
//      var p = var1
//      for _ in 0...(var3 - 1) {
//          var str  = "blah \(p)\n"
//          for _ in 1...20 {
//              gra = SomeClass(size: n, pro: p)!
//              m = self.doSomething(gra)
//              switch m {
//              case .First(let dat):
//                  str = str + "Blah:\n\(self.arrayF(dat, transform: {"blah\($0)blah\($1)=blah"}))" + "\n\n" + str
//              case .Second(let odat):
//                  str = str + "Blah\(odat.count) blah\(self.arrayF(odat, transform: {"bl\($1)"}))" + "\n\n" + str
//              }
//              dispatch_async(dispatch_get_main_queue()) {
//                  self.prog.incrementBy(1)
//              }
//          }
//          dispatch_async(dispatch_get_main_queue()) {
//              // update some UI
//              self.display.string = str + "\n" + (self.display.string ?? "")
//          }
//          p += var2
//      }
//  }

    //This works
    func theFunc(n: Int, var1: Double, var2: Double, var3: Int) {
        var m: AnEnum
        var gra: SomeClass
        var p = var1
        for _ in 0...(var3 - 1) {
            var str  = "blah \(p)\n"
            for _ in 1...20 {
                gra = SomeClass(size: n, pro: p)!
                m = self.doSomething(gra)
                switch m {
                case .First(let dat):
                    str = "Blah:\n\(self.arrayF(dat, transform: {"blah\($0)blah\($1)=blah"}))" + "\n\n" + str
                case .Second(let odat):
                    str = "Blah\(odat.count) blah\(self.arrayF(odat, transform: {"bl\($1)"}))" + "\n\n" + str
                }
                dispatch_async(dispatch_get_main_queue()) {
                    self.prog.incrementBy(1)
                }
            }
            dispatch_async(dispatch_get_main_queue()) {
                // update some UI
                self.display.string = str + "\n" + (self.display.string ?? "")
            }
            p += var2
        }
    }

    func doSomething(G: SomeClass) -> AnEnum {
        usleep(30000)
        if drand48() <= G.p {
            return AnEnum.First([0, 0])
        } else {
            return AnEnum.Second([1, 1, 1])
        }
    }

    enum AnEnum {
        case First([Int])
        case Second([Int])
    }

    func appOut(out: String?) {
        if out != nil {
            display.string = out! + "\n\n" + (display.string ?? "")
        }
    }

    func arrayF(array: [Int], transform: (index: Int, value: Int) -> String) -> String {
        let arr = Array(0...(array.count - 1))
        return "[\(arr.map{transform(index: $0, value: array[$0])}.joinWithSeparator(", "))]"
    }
}

如果这些代码在后台线程上运行,那么您的UI将不会冻结,除非您同时写入和读取str。我相信这就是您的问题所在,直到您知道str已经完成,才不要调度self.display.string = str + "\n" + (self.display.string ?? "")。这就是Objective C中原子操作的方便之处。 - Knight0fDragon
6
当然我们相信你的方法是有效的。但为了调查你的问题,一个“自包含”的示例将非常有帮助。 - Martin R
1
这只是纯粹的猜测(因为我们无法像@MartinR上面提到的那样复制您的示例),但Swift编译器可能(过于?)聪明,并将str = str +“Blah \(odat.count)”视为str + =“Blah \(odat.count)”。在后者中,str是一个被操作改变(但具有复制行为...)的inout变量,与使用+运算符有一些微妙的差别,后者使用运算符调用的返回值来覆盖/改变str。声称str为inout变量对str的竞争条件可能有一些影响。 - dfrib
我有一个可用的示例!太好了!请查看下一篇帖子/编辑。 - Richard Birkett
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - vittore
显示剩余2条评论
2个回答

3

既然你并没有提出问题,只是想说“你能相信吗?”,我告诉你,我当然可以相信,但是有很多情况下,在某些场景下,前置和后置操作的速度可能会快慢不一。以链表为例,如果你没有保留对列表最后一个元素的引用,则前置操作是O(1),而后置操作是O(N)。

我编写了一个代码片段来测试这个问题,经过5-6次运行,似乎没有显着差异,但是在我的机器上,前置操作仍然比后置操作慢10%。

有趣的是,在Swift中,针对你的情况,基本上有四种字符串连接方式:

  • 将新字符添加到累加器的前面 str = newstr + str
  • 将新字符添加到累加器的后面 str = str + newstr
  • 追加并修改 str.append(newstr)
  • 使用传统的数组作为字符串缓冲区,并一次性连接所有内容 a = []; a.append(x); str = a.joined(separator: " ")

在我的机器上,它们的执行时间似乎都差不多,典型的执行时间如下:

prepend
real    0m0.082s
user    0m0.060s
sys 0m0.018s

append
real    0m0.070s
user    0m0.049s
sys 0m0.018s

append mutate
real    0m0.075s
user    0m0.054s
sys 0m0.019s

join
real    0m0.086s
user    0m0.064s
sys 0m0.020s

追加操作是最快的。

您可以在我的 gist 中查看四种情况的代码 https://gist.github.com/ojosdegris/df72a94327d12a67fe65e5989f9dcc53

如果您在 Github 上查看 Swift 源代码,您会看到这样的内容:

@effects(readonly)
@_semantics("string.concat")
public static func + (lhs: String, rhs: String) -> String {
 if lhs.isEmpty {
  return rhs
 }
 var lhs = lhs
 lhs._core.append(rhs._core)
 return lhs
}

随着累加器字符串的增长,复制它的成本可能会更高。


我在XCTests中进行了自己的实验,因为看到简单的循环可以展示这一点。我发现将循环大小增加数个数量级会显示出更大的差异。转换到追加也没有好多少。 - Richard Birkett

1
维托尔的答案是正确的。查看Swift的String源代码(stdlib/public/core/String.swift),我们可以看到:
尽管Swift中的字符串具有值语义,但字符串使用写时复制(copy-on-write)策略来存储它们在缓冲区中的数据。然后,这个缓冲区可以被不同的字符串副本共享。只有当多个字符串实例正在使用相同的缓冲区时,才会在突变时懒惰地复制一个字符串的数据。因此,在任何突变操作序列中的第一个操作可能需要O(n)的时间和空间成本。
当一个字符串的连续存储填满时,必须分配一个新的缓冲区,并将数据移动到新的存储中。字符串缓冲区使用指数增长策略,使得在许多附加操作平均分布的情况下,附加到字符串的操作是一个常量时间操作。
根据维基百科的Copy-on-write
如果资源被复制但未修改,则无需创建新资源;该资源可以在副本和原始资源之间共享。

考虑到这一点,当执行str = str + "abc"时,编译器会对str进行浅复制,并将"abc"附加到其连续内存中。另一方面,str = "abc" + str创建了一个独立的实例,具有自己的数据唯一副本,因为它不再使用连续内存。


谢谢。我已经将正确的答案授予 @Vittore(谢谢!!),但是您提供了重要的 Swift 细节和来源,因此将赏金授予了您。 - Richard Birkett

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接