Skip to content

Commit cd5f0e4

Browse files
committed
Base64: Speedup encoding fix 3
- Pass UnsafeRawBufferPointers to NSData.base64EncodeBytes() and make the Data.base64EncodedString() and Data.base64EncodedData() methods call it directly. - There is considerable overhead in forwarding the methods from Data to NSData. Making the Data methods directly call NSData.base64EncodeBytes() removes this overhead.
1 parent 01699b6 commit cd5f0e4

File tree

2 files changed

+34
-16
lines changed

2 files changed

+34
-16
lines changed

Sources/Foundation/Data.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2514,22 +2514,37 @@ public struct Data : ReferenceConvertible, Equatable, Hashable, RandomAccessColl
25142514
///
25152515
/// - parameter options: The options to use for the encoding. Default value is `[]`.
25162516
/// - returns: The Base-64 encoded string.
2517-
@inlinable // This is @inlinable as trivially forwarding.
25182517
public func base64EncodedString(options: Data.Base64EncodingOptions = []) -> String {
2519-
return _representation.withInteriorPointerReference {
2520-
return $0.base64EncodedString(options: options)
2521-
}
2518+
let dataLength = self.count
2519+
if dataLength == 0 { return "" }
2520+
2521+
let capacity = NSData.estimateBase64Size(length: dataLength)
2522+
let ptr = UnsafeMutableRawPointer.allocate(byteCount: capacity, alignment: 4)
2523+
defer { ptr.deallocate() }
2524+
let buffer = UnsafeMutableRawBufferPointer(start: ptr, count: capacity)
2525+
let length = NSData.base64EncodeBytes(self, options: options, buffer: buffer)
2526+
2527+
return String(decoding: UnsafeRawBufferPointer(start: ptr, count: length), as: Unicode.UTF8.self)
25222528
}
25232529

25242530
/// Returns a Base-64 encoded `Data`.
25252531
///
25262532
/// - parameter options: The options to use for the encoding. Default value is `[]`.
25272533
/// - returns: The Base-64 encoded data.
2528-
@inlinable // This is @inlinable as trivially forwarding.
25292534
public func base64EncodedData(options: Data.Base64EncodingOptions = []) -> Data {
2530-
return _representation.withInteriorPointerReference {
2531-
return $0.base64EncodedData(options: options)
2535+
let dataLength = self.count
2536+
if dataLength == 0 { return Data() }
2537+
2538+
let capacity = NSData.estimateBase64Size(length: dataLength)
2539+
let ptr = UnsafeMutableRawPointer.allocate(byteCount: capacity, alignment: 4)
2540+
let outputBuffer = UnsafeMutableRawBufferPointer(start: ptr, count: capacity)
2541+
2542+
let length = self.withUnsafeBytes { inputBuffer in
2543+
NSData.base64EncodeBytes(inputBuffer, options: options, buffer: outputBuffer)
25322544
}
2545+
return Data(bytesNoCopy: ptr, count: length, deallocator: .custom({ (ptr, length) in
2546+
ptr.deallocate()
2547+
}))
25332548
}
25342549

25352550
// MARK: -

Sources/Foundation/NSData.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
604604

605605
// MARK: - Base64 Methods
606606

607-
private func estimateBase64Size(length: Int) -> Int {
607+
internal static func estimateBase64Size(length: Int) -> Int {
608608
// Worst case allow for 64bytes + \r\n per line 48 input bytes => 66 output bytes
609609
return ((length + 47) * 66) / 48
610610
}
@@ -614,7 +614,7 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
614614
let dataLength = self.length
615615
if dataLength == 0 { return "" }
616616

617-
let capacity = estimateBase64Size(length: dataLength)
617+
let capacity = NSData.estimateBase64Size(length: dataLength)
618618
let ptr = UnsafeMutableRawPointer.allocate(byteCount: capacity, alignment: 4)
619619
defer { ptr.deallocate() }
620620
let buffer = UnsafeMutableRawBufferPointer(start: ptr, count: capacity)
@@ -628,11 +628,13 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
628628
let dataLength = self.length
629629
if dataLength == 0 { return Data() }
630630

631-
let capacity = estimateBase64Size(length: dataLength)
631+
let inputBuffer = UnsafeRawBufferPointer(start: self.bytes, count: self.length)
632+
633+
let capacity = NSData.estimateBase64Size(length: dataLength)
632634
let ptr = UnsafeMutableRawPointer.allocate(byteCount: capacity, alignment: 4)
633-
let buffer = UnsafeMutableRawBufferPointer(start: ptr, count: capacity)
634-
let length = NSData.base64EncodeBytes(self, options: options, buffer: buffer)
635+
let outputBuffer = UnsafeMutableRawBufferPointer(start: ptr, count: capacity)
635636

637+
let length = NSData.base64EncodeBytes(inputBuffer, options: options, buffer: outputBuffer)
636638
return Data(bytesNoCopy: ptr, count: length, deallocator: .custom({ (ptr, length) in
637639
ptr.deallocate()
638640
}))
@@ -761,13 +763,14 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
761763
/**
762764
This method encodes data in Base64.
763765

764-
- parameter data: The NSData object you want to encode
766+
- parameter dataBuffer: The UnsafeRawBufferPointer buffer to encode
765767
- parameter options: Options for formatting the result
766768
- parameter buffer: The buffer to write the bytes into
767769
- returns: The number of bytes written into the buffer
768-
*/
769-
private static func base64EncodeBytes(_ data: NSData, options: Base64EncodingOptions = [], buffer: UnsafeMutableRawBufferPointer) -> Int {
770770

771+
NOTE: dataBuffer would be better expressed as a <T: Collection> where T.Element == UInt8, T.Index == Int but this currently gives much poorer performance.
772+
*/
773+
static func base64EncodeBytes(_ dataBuffer: UnsafeRawBufferPointer, options: Base64EncodingOptions = [], buffer: UnsafeMutableRawBufferPointer) -> Int {
771774
// Use a StaticString for lookup of values 0-63 -> ASCII values
772775
let base64Chars = StaticString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
773776
assert(base64Chars.utf8CodeUnitCount == 64)
@@ -811,7 +814,7 @@ open class NSData : NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
811814
}
812815

813816
// Read three bytes at a time, which convert to 4 ASCII characters, allowing for byte2 and byte3 being nil
814-
let dataBuffer = UnsafeRawBufferPointer(start: data.bytes, count: data.length)
817+
815818
var inputIndex = 0
816819
var outputIndex = 0
817820
var bytesLeft = dataBuffer.count

0 commit comments

Comments
 (0)