Skip to content

Commit 41cec28

Browse files
authored
Significantly improve performance of row decoding. (vapor#130) (vapor#134)
* Significantly improve performance of row decoding. Motivation: 1. `PostgreSQLRowDecoder`: `_KeyedDecodingContainer.decode` is called once for each key on the decoded object. With the current implementation, this performs a linear search on the `row` dictionary, which results in a total runtime (per object) of O(keys * columns), i.e. quadratic runtime ~O(keys^2). We replace the current linear search with one or two dictionary lookups if `tableOID != 0`, resulting in linear runtime (per object) in the number of keys (provided dictionary lookups can be assumed to take roughly constant time). 2. `PostgreSQLConnection.TableNameCache`: Most lookups are `tableName -> OID`. We accelerate that lookup by preparing a dictionary for that kind of lookup ahead of time, again replacing linear search. Effect: The time required for decoding ~5k objects with 9 fields each drops from ~0.4s on a Core i7-6700k (Release build) to ~0.2s, effectively doubling throughput. Optimization 1 contributes ~130 ms, Optimization 2 contributes ~70ms. * Whitespace fixes. * Comment fix. * More whitespace, sorry. * Implement `decodeIfPresent` to avoid two dictionary lookups per call. * Minor code simplification.
1 parent 5088dcb commit 41cec28

File tree

3 files changed

+64
-28
lines changed

3 files changed

+64
-28
lines changed

Sources/PostgreSQL/Codable/PostgreSQLRowDecoder.swift

+43-11
Original file line numberDiff line numberDiff line change
@@ -47,26 +47,48 @@ struct PostgreSQLRowDecoder {
4747
let codingPath: [CodingKey] = []
4848
let row: [PostgreSQLColumn: PostgreSQLData]
4949
let tableOID: UInt32
50-
let allKeys: [Key]
50+
var allKeys: [Key] {
51+
// Unlikely to be called (mostly present for protocol conformance), so we don't need to cache this property.
52+
return row.keys
53+
.compactMap { col in
54+
if tableOID == 0 || col.tableOID == tableOID || col.tableOID == 0 {
55+
return col.name
56+
} else {
57+
return nil
58+
}
59+
}.compactMap(Key.init(stringValue:))
60+
}
5161

5262
init(row: [PostgreSQLColumn: PostgreSQLData], tableOID: UInt32) {
5363
self.row = row
5464
self.tableOID = tableOID
55-
self.allKeys = row.keys.compactMap { col in
56-
if tableOID == 0 || col.tableOID == tableOID || col.tableOID == 0 {
57-
return col.name
58-
} else {
59-
return nil
60-
}
61-
}.compactMap(Key.init(stringValue:))
65+
}
66+
67+
private func data(for key: Key) -> PostgreSQLData? {
68+
let columnName = key.stringValue
69+
var column = PostgreSQLColumn(tableOID: self.tableOID, name: columnName)
70+
// First, check for an exact (tableOID, columnName) match.
71+
if let data = row[column] { return data }
72+
73+
if self.tableOID != 0 {
74+
// No column with our exact table OID; check for a (0, columnName) match instead.
75+
column.tableOID = 0
76+
return row[column]
77+
} else {
78+
// No (0, columnName) match; check via (slow!) linear search for _any_ matching column name,
79+
// regardless of tableOID.
80+
// Note: This path is hit in `PostgreSQLConnection.tableNames`, but luckily the `PGClass` only has
81+
// two keys, so the performance impact of linear search is acceptable there.
82+
return row.firstValue(tableOID: tableOID, name: columnName)
83+
}
6284
}
6385

6486
func contains(_ key: Key) -> Bool {
65-
return allKeys.contains { $0.stringValue == key.stringValue }
87+
return data(for: key) != nil
6688
}
6789

6890
func decodeNil(forKey key: Key) throws -> Bool {
69-
guard let data = row.firstValue(tableOID: tableOID, name: key.stringValue) else {
91+
guard let data = data(for: key) else {
7092
return true
7193
}
7294
switch data.storage {
@@ -76,12 +98,22 @@ struct PostgreSQLRowDecoder {
7698
}
7799

78100
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
79-
guard let data = row.firstValue(tableOID: tableOID, name: key.stringValue) else {
101+
guard let data = data(for: key) else {
80102
throw DecodingError.valueNotFound(T.self, .init(codingPath: codingPath + [key], debugDescription: "Could not decode \(T.self)."))
81103
}
82104
return try PostgreSQLDataDecoder().decode(T.self, from: data)
83105
}
84106

107+
// This specialization avoids two dictionary lookups (caused by calls to `contains` and `decodeNil`) present in
108+
// the default implementation of `decodeIfPresent`.
109+
func decodeIfPresent<T>(_ type: T.Type, forKey key: Key) throws -> T? where T : Decodable {
110+
guard let data = data(for: key) else { return nil }
111+
switch data.storage {
112+
case .null: return nil
113+
default: return try PostgreSQLDataDecoder().decode(T.self, from: data)
114+
}
115+
}
116+
85117
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
86118
fatalError()
87119
}

Sources/PostgreSQL/Column/PostgreSQLColumn.swift

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ extension PostgreSQLColumn: CustomStringConvertible {
2525

2626
extension Dictionary where Key == PostgreSQLColumn {
2727
/// Accesses the _first_ value from this dictionary with a matching field name.
28+
///
29+
/// - Note: This performs a linear search over the dictionary and thus is fairly slow.
2830
public func firstValue(tableOID: UInt32 = 0, name: String) -> Value? {
2931
for (column, data) in self {
3032
if (tableOID == 0 || column.tableOID == 0 || column.tableOID == tableOID) && column.name == name {

Sources/PostgreSQL/Connection/PostgreSQLConnection+TableNameCache.swift

+19-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
private struct PGClass: PostgreSQLTable {
2+
static let sqlTableIdentifierString = "pg_class"
3+
var oid: UInt32
4+
var relname: String
5+
}
6+
17
extension PostgreSQLConnection {
28
/// Caches table OID to string name associations.
39
public struct TableNameCache {
410
/// Stores table names. [OID: Name]
511
private let tableNames: [UInt32: String]
12+
/// Stores table OIDs. [Name: OID]
13+
/// Used to accelerate the Name -> OID lookup.
14+
private let tableOIDs: [String: UInt32]
615

716
/// Fetches the table name for a given table OID. Returns `nil` if no table with that OID is known.
817
///
@@ -19,17 +28,19 @@ extension PostgreSQLConnection {
1928
/// - name: Table name.
2029
/// - returns: Table OID.
2130
public func tableOID(name: String) -> UInt32? {
22-
for (key, val) in tableNames {
23-
if val == name {
24-
return key
25-
}
26-
}
27-
return nil
31+
return tableOIDs[name]
2832
}
2933

3034
/// Creates a new cache.
31-
init(_ tableNames: [UInt32: String]) {
35+
fileprivate init(_ tableClasses: [PGClass]) {
36+
var tableNames: [UInt32: String] = [:]
37+
var tableOIDs: [String: UInt32] = [:]
38+
for tableClass in tableClasses {
39+
tableNames[tableClass.oid] = tableClass.relname
40+
tableOIDs[tableClass.relname] = tableClass.oid
41+
}
3242
self.tableNames = tableNames
43+
self.tableOIDs = tableOIDs
3344
}
3445
}
3546

@@ -42,18 +53,9 @@ extension PostgreSQLConnection {
4253
if let existing = tableNameCache, !refresh {
4354
return future(existing)
4455
} else {
45-
struct PGClass: PostgreSQLTable {
46-
static let sqlTableIdentifierString = "pg_class"
47-
var oid: UInt32
48-
var relname: String
49-
}
5056
return select().column("oid").column("relname").from(PGClass.self).all().map { rows in
51-
var cache: [UInt32: String] = [:]
5257
let rows = try rows.map { try self.decode(PGClass.self, from: $0, table: nil) }
53-
for row in rows {
54-
cache[row.oid] = row.relname
55-
}
56-
let new = TableNameCache(cache)
58+
let new = TableNameCache(rows)
5759
self.tableNameCache = new
5860
return new
5961
}

0 commit comments

Comments
 (0)