Swift: 将字节从数组复制到另一个数组

3

我有一些性能敏感的代码,需要将一个数组的某个范围内的值复制到另一个数组的特定偏移量处。在Swift中,您能执行简单的移动内存操作吗?我一早上都在阅读这方面的内容,但是他们让在Swift中访问内存变得非常困难,我甚至不确定是否可能(显然我是Swift新手)。

我尝试使用Array.replaceSubrange,但它会创建一个底层有多少内存副本的巨大丑陋的代码块获取"Array slices",而且函数本身也很慢,根据目前的Swift情况,一个简单的memmove()将轻松解决问题。

以下是我认为可能的示例。

var src: [UInt32] = [1, 2, 3, 4]
var dest: [UInt32] = [0, 0, 0, 0]

dest.withUnsafeMutableBytes { destBytes in
    src.withUnsafeBytes { srcBytes in
       // for example copy 4 bytes starting at the address of destBytes[1]
       // from the address of srcBytes[1]
       movemem(&destBytes[1], &srcBytes[1], 4)
    }
}
// dest now should be [0, 2, 3, 0] assuming UInt32 is 2 bytes

"Array.replaceSubrange ... 根据 Swift 目前的表现,可能 很慢" - 你能提供一些具体的测量数据来证实这个假设吗? - Martin R
您的代码片段存在编译时错误。我正在使用Swift 4.0 - TheTiger
是的,我想将一个数组中偏移量为X字节的内容复制到另一个数组中。看起来你可以从Swift中调用memmove,但我需要从中获取“UnsafeRawPointer”。 - GenericPtr
抱歉,我的意思是“复制并替换/覆盖”,这基本上就是memmove所做的或者Array.replaceSubrange所做的。 - GenericPtr
“slice” 可能类似于 CFRange,需要创建并传递给函数。我不知道苹果是如何实现它的,所以我不想将其引入系统中。关键点是我的代码对性能敏感,因此我不关心可读性/安全性等等... 它只需要快速运行。 - GenericPtr
显示剩余17条评论
2个回答

2

假设问题是 需要将一个数组中的一系列值复制到另一个数组的特定偏移位置。

var src: [UInt32] = [1, 2, 3, 4]
var dest: [UInt32] = [0, 0, 0, 0]

let rangeOfSrc = [1...2] /// Will get from 1st to 2nd so that 2, 3
dest.insert(contentsOf: rangeOfSrc, at: 2) /// Will insert this range at 2nd position of the dest array
print(dest)

输出: [0, 0, 2, 3, 0, 0]

想要了解更多详细信息,请参考此文档

编辑2: 如果您想替换范围而不是插入。

dest.replaceSubrange(1...2, with: src[1...2])
print(dest)

输出:[0, 2, 3, 0]

编辑1:memmove

memmove(&dest[0], &src[0], 4)
print(dest)

输出: [1, 0, 0, 0]

编辑3

src.withUnsafeBufferPointer {(result) in
     memmove(&dest[0], result.baseAddress, 8)
     print(dest)
}

输出:[1,2,0,0]

是的,插入不是我想要的,因为它会增加数组的大小。我没有想到尝试我打的那个“伪”代码,但它确实可以编译。 :) 然而,有些东西出了问题,因为MemoryLayout<UInt32>.stride返回4,但是"memmove(&dest[0], &src[0], 4 * 2)"返回dest = [1, 16777216, 0, 0]。出了些问题,我不知道在Swift中可以像这样访问内存,所以谁知道它正在寻址什么。 - GenericPtr
@GenericPtr 请参考我的回答中的Edit2。它将替换范围而不是插入。你可能想要相同的操作。 - TheTiger
这不是memmove的工作方式。最后一个参数是从src指针复制的字节数,因此8应该是数组的2个元素,并产生[1, 2, 0, 0]。我对Swift还很陌生,但我几乎可以确定你不能像那样获取变量的地址,并期望实际上获取它们在堆栈中的位置。我们正在处理代码中的某些内容,但可能不是堆栈。我非常确定我们需要使用withUnsafeXXX调用。 - GenericPtr
是的,& 是元素的内存地址。根据苹果文档,它应该在 Swift 中工作。 - TheTiger
看看我的最新答案更新。现在很接近了,现在可能已经足够好了。谢谢大家。 - GenericPtr
显示剩余7条评论

1

我有点回答自己的问题,尽管我认为这不是最好的解决方案。由于不熟悉Swift,我认为这可能是我们能做到的最好的方法,但是使用UnsafeMutableRawPointer(mutating:)进行memmove()转换存在潜在的内存分配问题。必须有一种方法来进行转换,否则将该分配插入我的代码中肯定会影响性能。

如果有人知道如何避免对UnsafeMutableRawPointer的转换,请告诉我。

        var src: [UInt32] = [1, 2, 3, 4]
        var dest: [UInt32] = [0, 0, 0, 0]
        let elemSize = MemoryLayout<UInt32>.stride

        dest.withUnsafeBytes { (destBuffer: UnsafeRawBufferPointer) in
            src.withUnsafeBytes { (srcBuffer: UnsafeRawBufferPointer) in

                // memmove requires UnsafeMutableRawPointer but how do we avoid this allocation?
                // maybe Swift optimizes this somehow but it looks really bad from here
                let destPtr = UnsafeMutableRawPointer(mutating: destBuffer.baseAddress)
                let destOffset = destPtr! + elemSize
                let srcOffset = srcBuffer.baseAddress! + 0

                // copy 2 elements from src[0] to dest[1]
                memmove(destOffset, srcOffset, elemSize * 2)
            }
        }

        print(dest) // [0, 1, 2, 0]

编辑 1:现在越来越接近了。显然,replaceSubrange() 比 memmove() 慢得多,实际上慢了6-7倍。相比之下,字节计数越小,replaceSubrange() 的速度就越快。在实际示例中,在执行所有 memmove() 调用之前,您只需要一次获取数组字节,因此实际上比这更快。

replaceSubrange: 0.750978946685791

memmove: 0.139282941818237

func TestMemmove() {
    var src: [UInt32] = Array(repeating: 1, count: 1000)
    var dest: [UInt32] = Array(repeating: 0, count: 1000)

    let elemSize = MemoryLayout<UInt32>.stride
    let testCycles = 100000
    let rows = 200

    var startTime = CFAbsoluteTimeGetCurrent()
    for _ in 0..<testCycles {
        dest.replaceSubrange(1...1+rows, with: src[0...rows])
    }
    var endTime = CFAbsoluteTimeGetCurrent()
    print("replaceSubrange:  \(endTime - startTime)")

    startTime = CFAbsoluteTimeGetCurrent()
    for _ in 0..<testCycles {
        dest.withUnsafeMutableBytes { destBytes in
            src.withUnsafeMutableBytes { srcBytes in
                let destOffset = destBytes.baseAddress! + elemSize
                let srcOffset = srcBytes.baseAddress! + 0
                memmove(destOffset, srcOffset, elemSize * rows)
            }
        }
    }
    endTime = CFAbsoluteTimeGetCurrent()
    print("memmove:  \(endTime - startTime)")    
}

编辑2:在所有这些愚蠢的事情之后,从C中调用memmove是最快的。Swift将指针传递给数组的第一个元素,然后您可以使用C中的指针算术来处理所需的偏移量,这需要在Swift中进行.withUnsafeXXX调用(这可能分配了一些类包装器)。

结论是Swift很慢,因此在任何性能敏感的代码中都要切换到C。

BlockMove:0.0957469940185547 replaceSubrange:1.89903497695923 memmove:0.136561989784241

// from .c file bridged to Swift
void BlockMove (void* dest, int destOffset, const void* src, int srcOffset, size_t count) {
    memmove(dest + destOffset, src + srcOffset, count);
}

func TestMemmove() {
var src: [UInt32] = Array(repeating: 1, count: 1000)
var dest: [UInt32] = Array(repeating: 0, count: 1000)

let elemSize = MemoryLayout<UInt32>.stride
let testCycles = 100000
let rows = 500
var startTime: CFAbsoluteTime = 0
var endTime: CFAbsoluteTime = 0

// BlockMove (from c)
startTime = CFAbsoluteTimeGetCurrent()
for _ in 0..<testCycles {
    BlockMove(&dest, Int32(elemSize), &src, 0, Int32(elemSize * rows))
}
endTime = CFAbsoluteTimeGetCurrent()
print("BlockMove:  \(endTime - startTime)")

// replaceSubrange
startTime = CFAbsoluteTimeGetCurrent()
for _ in 0..<testCycles {
    dest.replaceSubrange(1...1+rows, with: src[0...rows])
}
endTime = CFAbsoluteTimeGetCurrent()
print("replaceSubrange:  \(endTime - startTime)")

// memmove
startTime = CFAbsoluteTimeGetCurrent()
for _ in 0..<testCycles {
    dest.withUnsafeMutableBytes { destBytes in
        src.withUnsafeMutableBytes { srcBytes in
            let destOffset = destBytes.baseAddress! + elemSize
            let srcOffset = srcBytes.baseAddress! + 0
            memmove(destOffset, srcOffset, elemSize * rows)
        }
    }
}
endTime = CFAbsoluteTimeGetCurrent()
print("memmove:  \(endTime - startTime)")

}


UnsafeMutableRawPointer(mutating:) 不会分配内存。但是,您可以通过使用 dest.withUnsafeMutableBytes 来摆脱该转换,这将在闭包中为您提供一个 可变 的缓冲区指针。 - Martin R
您不需要获取destBuffer和进行这个计算,请查看我的回答中的Edit3 - TheTiger

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