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.experimental.allocator : makeArray, stateSize; 17 import std.experimental.allocator.common : stateSize; 18 import std.experimental.allocator.gc_allocator : GCAllocator; 19 import std.traits; 20 21 /// Optimized appender. Not copyable. 22 struct FastAppender(I, Allocator = GCAllocator) 23 { 24 static assert(T.sizeof == 1, "TODO"); 25 26 private: 27 enum PAGE_SIZE = 4096; 28 enum MIN_SIZE = PAGE_SIZE / 2 + 1; // smallest size that can expand 29 30 alias Unqual!I T; 31 32 T* cursor, start, end; 33 bool unique; // Holding a unique reference to the buffer? 34 35 void reserve(size_t len) 36 { 37 immutable size = cursor-start; 38 immutable newSize = size + len; 39 immutable capacity = end-start; 40 41 if (start) 42 { 43 size_t extended = 0; 44 static if (is(Allocator == GCAllocator)) 45 { 46 // std.allocator does not have opportunistic extend 47 import core.memory; 48 extended = GC.extend(start, newSize, newSize * 2); 49 } 50 else 51 { 52 static if (is(hasMember!(Allocator, "expand"))) 53 { 54 auto buf = start[0..capacity]; 55 if (allocator.expand(buf, newSize)) 56 { 57 assert(buf.ptr == start); 58 extended = buf.length; 59 } 60 } 61 } 62 if (extended) 63 { 64 end = start + extended; 65 return; 66 } 67 } 68 69 auto newCapacity = newSize < MIN_SIZE ? MIN_SIZE : 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 { 302 FastAppender!(char, Allocator) a; 303 assert(a.get == ""); 304 a.put('a', "bcd", 'e'); 305 assert(a.get == "abcde"); 306 a.clear(); 307 assert(a.get == ""); 308 a.allocate(3)[] = 'x'; 309 assert(a.get == "xxx"); 310 } 311 } 312 313 /// UFCS shim for classic output ranges, which only take a single-argument put. 314 void putEx(R, U...)(auto ref R r, U items) 315 { 316 foreach (item; items) 317 { 318 static if (is(typeof(r.put(item)))) 319 r.put(item); 320 else 321 static if (is(typeof(r.put(item[])))) 322 r.put(item[]); 323 else 324 static if (is(typeof({ foreach (c; item) r.put(c); }))) 325 foreach (c; item) 326 r.put(c); 327 else 328 static if (is(typeof(r.put((&item)[0..1])))) 329 r.put((&item)[0..1]); 330 else 331 static assert(false, "Can't figure out how to put " ~ typeof(item).stringof ~ " into a " ~ R.stringof); 332 } 333 }