Skip to content

[SR-9604] Array initialisation from UnsafeRawBufferPointer takes 10x longer than from UnsafeBufferPointer<UInt8> #52050

@weissi

Description

@weissi
Previous ID SR-9604
Radar rdar://problem/52529574
Original Reporter @weissi
Type Bug
Status Resolved
Resolution Done
Additional Detail from JIRA
Votes 0
Component/s Compiler, Standard Library
Labels Bug, Performance
Assignee @eeckstein
Priority Medium

md5: 93fe6fdccf1fe595e6c2a712aacb4092

Issue Description:

John Connolly noticed a 4x slowdown of his NIO Redis driver (repro in https://github.com/John-Connolly/nio-performance) when switching from NIO 1.8.0 to NIO 1.9.0.
Turns out that ByteBuffer.getBytes is a lot slower in NIO 1.9.0 which caused all the slowdown.

The only change in getBytes however was from switching

            Array.init(UnsafeBufferPointer<UInt8>(start: ptr.baseAddress?.advanced(by: index).assumingMemoryBound(to: UInt8.self),
                                                  count: length))

to

            Array<UInt8>(ptr[index..<(index+length)])

as part of this PR: apple/swift-nio#524

This can also be reproduced super easily without NIO just standalone with this program:

import Foundation

let origin: [UInt8] = Array(repeating: 0xab, count: 1024 * 1024)

@inline(never)
func do1() -> Int {
    let start = Date()
    var x = 0
    for _ in 0..<1000 {
        x += origin.withUnsafeBytes { urbp in
            Array(urbp).count
        }
    }
    let end = Date()
    print(end.timeIntervalSince(start))
    return x
}

@inline(never)
func do2() -> Int {
    let start = Date()
    var x = 0
    for _ in 0..<1000 {
        x += origin.withUnsafeBufferPointer { ubp in
            Array(ubp).count
        }
    }
    let end = Date()
    print(end.timeIntervalSince(start))
    return x
}

@inline(never)
func do3() -> Int {
    let start = Date()
    var x = 0
    for _ in 0..<1000 {
        x += origin.withUnsafeBytes { urbp in
            Array(urbp[...]).count
        }
    }
    let end = Date()
    print(end.timeIntervalSince(start))
    return x
}

print("as UnsafeRawBufferPointer")
let one = do1()
print("as UnsafeBufferPointer<UInt8>")
let two = do2()
print("as Slice<UnsafeRawBufferPointer>")
let three = do3()
precondition(one == two)
precondition(one == three)

running this gives:

Swift 4.2.1

$ swift -O test.swift
as UnsafeRawBufferPointer
0.5475319623947144
as UnsafeBufferPointer<UInt8>
0.0333859920501709
as Slice<UnsafeRawBufferPointer>
0.7531230449676514

Swift 5 dev

$ /Library/Developer/Toolchains/swift-5.0-DEVELOPMENT-SNAPSHOT-2018-12-28-a.xctoolchain/usr/bin/swift -O test.swift
as UnsafeRawBufferPointer
0.5512470006942749
as UnsafeBufferPointer<UInt8>
0.03376805782318115
as Slice<UnsafeRawBufferPointer>
0.6322070360183716

Essentially, there seems to be a fast path missing that allows us to use memcpy instead of byte-by-byte copy when using UnsafeRawBufferPointer. Slices perform even worse...

CC @moiseev/@atrick/@airspeedswift/@milseman

Metadata

Metadata

Assignees

Labels

bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.compilerThe Swift compiler itselfperformancestandard libraryArea: Standard library umbrella

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions