1 /** 2 * Optimized copying appender, no chaining 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.utils.appender; 15 16 import std.algorithm.comparison : max; 17 import std.experimental.allocator : makeArray, stateSize; 18 import std.experimental.allocator.common : stateSize; 19 import std.experimental.allocator.gc_allocator : GCAllocator; 20 import std.traits; 21 22 /// Optimized appender. Not copyable. 23 struct FastAppender(I, Allocator = GCAllocator) 24 { 25 private: 26 enum PAGE_SIZE = 4096; 27 enum MIN_SIZE = PAGE_SIZE / 2 + 1; // smallest size that can expand 28 29 alias Unqual!I T; 30 31 T* cursor, start, end; 32 bool unique; // Holding a unique reference to the buffer? 33 34 void reserve(size_t len) 35 { 36 immutable size = cursor-start; 37 immutable newSize = size + len; 38 immutable capacity = end-start; 39 40 if (start) 41 { 42 size_t extended = 0; 43 static if (is(Allocator == GCAllocator)) 44 { 45 // std.allocator does not have opportunistic extend 46 import core.memory : GC; 47 extended = GC.extend(start, newSize * T.sizeof, newSize * 2 * T.sizeof) / T.sizeof; 48 } 49 else 50 { 51 static if (hasMember!(Allocator, "expand")) 52 { 53 void[] buf = start[0..capacity]; 54 if (allocator.expand(buf, newSize * T.sizeof)) 55 { 56 assert(buf.ptr == start); 57 extended = buf.length / T.sizeof; 58 } 59 } 60 } 61 if (extended) 62 { 63 end = start + extended; 64 return; 65 } 66 } 67 68 enum minSize = max(1, MIN_SIZE / T.sizeof); 69 auto newCapacity = newSize < minSize ? minSize : newSize * 2; 70 71 version(none) 72 { 73 auto bi = GC.qalloc(newCapacity * T.sizeof, (typeid(T[]).next.flags & 1) ? 0 : GC.BlkAttr.NO_SCAN); 74 auto newStart = cast(T*)bi.base; 75 newCapacity = bi.size; 76 } 77 else 78 { 79 auto buf = allocator.makeArray!T(newCapacity); 80 assert(buf.length == newCapacity); 81 auto newStart = buf.ptr; 82 } 83 84 newStart[0..size] = start[0..size]; 85 if (unique) 86 allocator.deallocate(start[0..capacity]); 87 start = newStart; 88 cursor = start + size; 89 end = start + newCapacity; 90 unique = true; 91 } 92 93 public: 94 /// The allocator. 95 static if (stateSize!Allocator) 96 Allocator allocator; 97 else 98 alias allocator = Allocator.instance; 99 100 static if (stateSize!Allocator == 0) 101 { 102 /// Preallocate 103 this(size_t capacity) 104 { 105 reserve(capacity); 106 } 107 } 108 109 /// Start with a given buffer 110 this(I[] arr) 111 { 112 start = cursor = cast(T*)arr.ptr; 113 end = start + arr.length; 114 } 115 116 @disable this(this); 117 118 ~this() 119 { 120 if (cursor && unique) 121 allocator.deallocate(start[0..end-start]); 122 } 123 124 /// Put elements. 125 /// Accepts any number of items (and will allocate at most once per call). 126 /// Items can be of the element type (I), or arrays. 127 void putEx(U...)(U items) 128 if (CanPutAll!U) 129 { 130 // TODO: check for static if length is 1 131 auto cursorEnd = cursor; 132 foreach (item; items) 133 static if (is(typeof(cursor[0] = item))) 134 cursorEnd++; 135 else 136 // TODO: is this too lax? it allows passing static arrays by value 137 static if (is(typeof(cursor[0..1] = item[0..1]))) 138 cursorEnd += item.length; 139 else 140 static assert(0, "Can't put " ~ typeof(item).stringof); 141 142 if (cursorEnd > end) 143 { 144 auto len = cursorEnd - cursor; 145 reserve(len); 146 cursorEnd = cursor + len; 147 } 148 auto cursor = this.cursor; 149 this.cursor = cursorEnd; 150 151 static if (items.length == 1) 152 { 153 alias items[0] item; 154 static if (is(typeof(cursor[0] = item))) 155 cursor[0] = item; 156 else 157 cursor[0..item.length] = item[]; 158 } 159 else 160 { 161 foreach (item; items) 162 static if (is(typeof(cursor[0] = item))) 163 *cursor++ = item; 164 else 165 static if (is(typeof(cursor[0..1] = item[0..1]))) 166 { 167 cursor[0..item.length] = item[]; 168 cursor += item.length; 169 } 170 } 171 } 172 173 alias put = putEx; /// Output range shim. 174 175 /// Unsafe. Use together with preallocate(). 176 void uncheckedPut(U...)(U items) @system 177 if (CanPutAll!U) 178 { 179 auto cursor = this.cursor; 180 181 foreach (item; items) 182 static if (is(typeof(cursor[0] = item))) 183 *cursor++ = item; 184 else 185 static if (is(typeof(cursor[0..1] = item[0..1]))) 186 { 187 cursor[0..item.length] = item; 188 cursor += item.length; 189 } 190 191 this.cursor = cursor; 192 } 193 194 /// Ensure we can append at least `len` more bytes before allocating. 195 void preallocate(size_t len) 196 { 197 if (end - cursor < len) 198 reserve(len); 199 } 200 201 /// Allocate a number of bytes, without initializing them, 202 /// and return the slice to be filled in. 203 /// The slice reference is temporary, and valid until the next allocation. 204 T[] allocate(size_t len) @system 205 { 206 auto cursorEnd = cursor + len; 207 if (cursorEnd > end) 208 { 209 reserve(len); 210 cursorEnd = cursor + len; 211 } 212 auto result = cursor[0..len]; 213 cursor = cursorEnd; 214 return result; 215 } 216 217 private template CanPutAll(U...) 218 { 219 static if (U.length==0) 220 enum bool CanPutAll = true; 221 else 222 { 223 enum bool CanPutAll = 224 ( 225 is(typeof(cursor[0 ] = U[0].init )) || 226 is(typeof(cursor[0..1] = U[0].init[0..1])) 227 ) && CanPutAll!(U[1..$]); 228 } 229 } 230 231 /// `~=` support. 232 void opOpAssign(string op, U)(U item) 233 if (op=="~" && is(typeof(put!U))) 234 { 235 put(item); 236 } 237 238 /// Get a reference to the buffer. 239 /// Ownership of the buffer is passed to the caller 240 /// (Appender will not deallocate it after this call). 241 I[] get() 242 { 243 unique = false; 244 return peek(); 245 } 246 247 /// As with `get`, but ownership is preserved. 248 /// The return value is valid until the next allocation, 249 /// or until Appender is destroyed. 250 I[] peek() 251 { 252 return cast(I[])start[0..cursor-start]; 253 } 254 255 /// How many items can be appended without a reallocation. 256 @property size_t capacity() 257 { 258 return end-start; 259 } 260 261 /// Resize backing buffer to the given capacity. 262 @property void capacity(size_t value) 263 { 264 immutable current = end - start; 265 assert(value >= current, "Cannot shrink capacity"); 266 reserve(value - length); 267 } 268 269 /// How many items have been written so far. 270 @property size_t length() 271 { 272 return cursor-start; 273 } 274 275 static if (is(I == T)) // mutable types only 276 { 277 /// Set the length (up to the current capacity). 278 @property void length(size_t value) 279 { 280 if (start + value > end) 281 preallocate(start + value - end); 282 cursor = start + value; 283 assert(cursor <= end); 284 } 285 286 /// Effectively empties the data, but preserves the storage for reuse. 287 /// Same as setting length to 0. 288 void clear() 289 { 290 cursor = start; 291 } 292 } 293 } 294 295 unittest 296 { 297 import std.meta : AliasSeq; 298 import std.experimental.allocator.mallocator; 299 300 foreach (Allocator; AliasSeq!(GCAllocator, Mallocator)) 301 foreach (C; AliasSeq!(char, wchar, dchar)) 302 { 303 FastAppender!(C, Allocator) a; 304 assert(a.get == ""); 305 immutable C[] s = "bcd"; 306 a.put(C('a'), s, C('e')); 307 assert(a.get == "abcde"); 308 a.clear(); 309 assert(a.get == ""); 310 a.allocate(3)[] = 'x'; 311 assert(a.get == "xxx"); 312 } 313 } 314 315 /// UFCS shim for classic output ranges, which only take a single-argument put. 316 void putEx(R, U...)(auto ref R r, U items) 317 { 318 foreach (item; items) 319 { 320 static if (is(typeof(r.put(item)))) 321 r.put(item); 322 else 323 static if (is(typeof(r.put(item[])))) 324 r.put(item[]); 325 else 326 static if (is(typeof({ foreach (c; item) r.put(c); }))) 327 foreach (c; item) 328 r.put(c); 329 else 330 static if (is(typeof(r.put((&item)[0..1])))) 331 r.put((&item)[0..1]); 332 else 333 static assert(false, "Can't figure out how to put " ~ typeof(item).stringof ~ " into a " ~ R.stringof); 334 } 335 }