Merge lp:~therve/storm/cache-compile into lp:storm
- cache-compile
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 400 | ||||
Proposed branch: | lp:~therve/storm/cache-compile | ||||
Merge into: | lp:storm | ||||
Diff against target: |
282 lines (+40/-26) 4 files modified
storm/expr.py (+14/-5) storm/info.py (+10/-11) tests/expr.py (+7/-1) tests/info.py (+9/-9) |
||||
To merge this branch: | bzr merge lp:~therve/storm/cache-compile | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Approve | ||
Jamu Kakar (community) | Approve | ||
Free Ekanayaka (community) | Approve | ||
Review via email: mp+71473@code.launchpad.net |
Commit message
Description of the change
Here it is. The branch adds a new attribute on Table and Column to cache the compilation. AFAIU, those compilations don't depend on the state, so we can store directly the value. It also changes the ClassInfo to store the table as a Table object instead of a SQLToken, to go through the compile_table logic.
I made a quick benchmark to show the gains:
10 inserts, 3 colums | 1.68s | 1.55s | 7.8%
100 insersts, 3 columns | 14.15s | 12.79s | 9.6%
20 insersts, 10 columns | 5.38s | 4.71s | 12.4%
I think it's worth it considering the low impact of the changes.
Thomas Herve (therve) wrote : | # |
Gustavo: sorry this was explicit, but it's run 1000 times over.
Thomas Herve (therve) wrote : | # |
Here's the script I used, with the last run: http://
Free Ekanayaka (free.ekanayaka) wrote : | # |
Hi Thomas,
that's indeed significantly faster for use cases similar to the one in the attached script.
+1
[1]
- self.table = SQLToken(
+ self.table = Table(self.table)
I guess this is to make use of the cache? Are SQLToken and Table fully interchangeable in this case?
[2]
It'd be good to add a couple of unit tests for the new behavior.
- 399. By Thomas Herve
-
Add tests for compile_cache value
Thomas Herve (therve) wrote : | # |
[1] Yes, it's for using the cache. table has been documented as an expression, so I don't see any problem changing it. The compilation passes token=True which results to the same result.
[2] I added some tests.
Thanks!
Jamu Kakar (jkakar) wrote : | # |
Looks good to me, +1!
Gustavo Niemeyer (niemeyer) wrote : | # |
Thomas, I mentioned 1000 inserts, not running it 1000 times.
Either way, sounds like it's improved things in practice, so +1.
Thomas Herve (therve) wrote : | # |
Right, with 1000 inserts
Branch: 0.227118968964
Trunk: 0.256669044495
So around 11% faster.
Preview Diff
1 | === modified file 'storm/expr.py' | |||
2 | --- storm/expr.py 2011-05-16 10:39:36 +0000 | |||
3 | +++ storm/expr.py 2011-08-24 11:55:39 +0000 | |||
4 | @@ -800,7 +800,8 @@ | |||
5 | 800 | @ivar variable_factory: Factory producing C{Variable} instances typed | 800 | @ivar variable_factory: Factory producing C{Variable} instances typed |
6 | 801 | according to this column. | 801 | according to this column. |
7 | 802 | """ | 802 | """ |
9 | 803 | __slots__ = ("name", "table", "primary", "variable_factory") | 803 | __slots__ = ("name", "table", "primary", "variable_factory", |
10 | 804 | "compile_cache") | ||
11 | 804 | 805 | ||
12 | 805 | def __init__(self, name=Undef, table=Undef, primary=False, | 806 | def __init__(self, name=Undef, table=Undef, primary=False, |
13 | 806 | variable_factory=None): | 807 | variable_factory=None): |
14 | @@ -808,6 +809,7 @@ | |||
15 | 808 | self.table = table | 809 | self.table = table |
16 | 809 | self.primary = int(primary) | 810 | self.primary = int(primary) |
17 | 810 | self.variable_factory = variable_factory or Variable | 811 | self.variable_factory = variable_factory or Variable |
18 | 812 | self.compile_cache = None | ||
19 | 811 | 813 | ||
20 | 812 | @compile.when(Column) | 814 | @compile.when(Column) |
21 | 813 | def compile_column(compile, column, state): | 815 | def compile_column(compile, column, state): |
22 | @@ -819,11 +821,15 @@ | |||
23 | 819 | alias = state.aliases.get(column) | 821 | alias = state.aliases.get(column) |
24 | 820 | if alias is not None: | 822 | if alias is not None: |
25 | 821 | return compile(alias.name, state, token=True) | 823 | return compile(alias.name, state, token=True) |
27 | 822 | return compile(column.name, state, token=True) | 824 | if column.compile_cache is None: |
28 | 825 | column.compile_cache = compile(column.name, state, token=True) | ||
29 | 826 | return column.compile_cache | ||
30 | 823 | state.push("context", COLUMN_PREFIX) | 827 | state.push("context", COLUMN_PREFIX) |
31 | 824 | table = compile(column.table, state, token=True) | 828 | table = compile(column.table, state, token=True) |
32 | 825 | state.pop() | 829 | state.pop() |
34 | 826 | return "%s.%s" % (table, compile(column.name, state, token=True)) | 830 | if column.compile_cache is None: |
35 | 831 | column.compile_cache = compile(column.name, state, token=True) | ||
36 | 832 | return "%s.%s" % (table, column.compile_cache) | ||
37 | 827 | 833 | ||
38 | 828 | @compile_python.when(Column) | 834 | @compile_python.when(Column) |
39 | 829 | def compile_python_column(compile, column, state): | 835 | def compile_python_column(compile, column, state): |
40 | @@ -870,14 +876,17 @@ | |||
41 | 870 | 876 | ||
42 | 871 | 877 | ||
43 | 872 | class Table(FromExpr): | 878 | class Table(FromExpr): |
45 | 873 | __slots__ = ("name",) | 879 | __slots__ = ("name", "compile_cache") |
46 | 874 | 880 | ||
47 | 875 | def __init__(self, name): | 881 | def __init__(self, name): |
48 | 876 | self.name = name | 882 | self.name = name |
49 | 883 | self.compile_cache = None | ||
50 | 877 | 884 | ||
51 | 878 | @compile.when(Table) | 885 | @compile.when(Table) |
52 | 879 | def compile_table(compile, table, state): | 886 | def compile_table(compile, table, state): |
54 | 880 | return compile(table.name, state, token=True) | 887 | if table.compile_cache is None: |
55 | 888 | table.compile_cache = compile(table.name, state, token=True) | ||
56 | 889 | return table.compile_cache | ||
57 | 881 | 890 | ||
58 | 882 | 891 | ||
59 | 883 | class JoinExpr(FromExpr): | 892 | class JoinExpr(FromExpr): |
60 | 884 | 893 | ||
61 | === modified file 'storm/info.py' | |||
62 | --- storm/info.py 2008-06-20 06:50:47 +0000 | |||
63 | +++ storm/info.py 2011-08-24 11:55:39 +0000 | |||
64 | @@ -18,11 +18,11 @@ | |||
65 | 18 | # You should have received a copy of the GNU Lesser General Public License | 18 | # You should have received a copy of the GNU Lesser General Public License |
66 | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
67 | 20 | # | 20 | # |
69 | 21 | from weakref import ref, WeakKeyDictionary | 21 | from weakref import ref |
70 | 22 | 22 | ||
71 | 23 | from storm.exceptions import ClassInfoError | 23 | from storm.exceptions import ClassInfoError |
74 | 24 | from storm.expr import Expr, FromExpr, Column, Desc, TABLE | 24 | from storm.expr import Column, Desc, TABLE |
75 | 25 | from storm.expr import SQLToken, CompileError, compile | 25 | from storm.expr import compile, Table |
76 | 26 | from storm.event import EventSystem | 26 | from storm.event import EventSystem |
77 | 27 | from storm import Undef, has_cextensions | 27 | from storm import Undef, has_cextensions |
78 | 28 | 28 | ||
79 | @@ -40,9 +40,11 @@ | |||
80 | 40 | obj_info = ObjectInfo(obj) | 40 | obj_info = ObjectInfo(obj) |
81 | 41 | return obj.__dict__.setdefault("__storm_object_info__", obj_info) | 41 | return obj.__dict__.setdefault("__storm_object_info__", obj_info) |
82 | 42 | 42 | ||
83 | 43 | |||
84 | 43 | def set_obj_info(obj, obj_info): | 44 | def set_obj_info(obj, obj_info): |
85 | 44 | obj.__dict__["__storm_object_info__"] = obj_info | 45 | obj.__dict__["__storm_object_info__"] = obj_info |
86 | 45 | 46 | ||
87 | 47 | |||
88 | 46 | def get_cls_info(cls): | 48 | def get_cls_info(cls): |
89 | 47 | if "__storm_class_info__" in cls.__dict__: | 49 | if "__storm_class_info__" in cls.__dict__: |
90 | 48 | # Can't use attribute access here, otherwise subclassing won't work. | 50 | # Can't use attribute access here, otherwise subclassing won't work. |
91 | @@ -51,6 +53,7 @@ | |||
92 | 51 | cls.__storm_class_info__ = ClassInfo(cls) | 53 | cls.__storm_class_info__ = ClassInfo(cls) |
93 | 52 | return cls.__storm_class_info__ | 54 | return cls.__storm_class_info__ |
94 | 53 | 55 | ||
95 | 56 | |||
96 | 54 | class ClassInfo(dict): | 57 | class ClassInfo(dict): |
97 | 55 | """Persistent storm-related information of a class. | 58 | """Persistent storm-related information of a class. |
98 | 56 | 59 | ||
99 | @@ -71,7 +74,7 @@ | |||
100 | 71 | self.cls = cls | 74 | self.cls = cls |
101 | 72 | 75 | ||
102 | 73 | if isinstance(self.table, basestring): | 76 | if isinstance(self.table, basestring): |
104 | 74 | self.table = SQLToken(self.table) | 77 | self.table = Table(self.table) |
105 | 75 | 78 | ||
106 | 76 | pairs = [] | 79 | pairs = [] |
107 | 77 | for attr in dir(cls): | 80 | for attr in dir(cls): |
108 | @@ -79,7 +82,6 @@ | |||
109 | 79 | if isinstance(column, Column): | 82 | if isinstance(column, Column): |
110 | 80 | pairs.append((attr, column)) | 83 | pairs.append((attr, column)) |
111 | 81 | 84 | ||
112 | 82 | |||
113 | 83 | pairs.sort() | 85 | pairs.sort() |
114 | 84 | 86 | ||
115 | 85 | self.columns = tuple(pair[1] for pair in pairs) | 87 | self.columns = tuple(pair[1] for pair in pairs) |
116 | @@ -121,7 +123,6 @@ | |||
117 | 121 | self.primary_key_pos = tuple(id_positions[id(column)] | 123 | self.primary_key_pos = tuple(id_positions[id(column)] |
118 | 122 | for column in self.primary_key) | 124 | for column in self.primary_key) |
119 | 123 | 125 | ||
120 | 124 | |||
121 | 125 | __order__ = getattr(cls, "__storm_order__", None) | 126 | __order__ = getattr(cls, "__storm_order__", None) |
122 | 126 | if __order__ is None: | 127 | if __order__ is None: |
123 | 127 | self.default_order = Undef | 128 | self.default_order = Undef |
124 | @@ -151,7 +152,7 @@ | |||
125 | 151 | __hash__ = object.__hash__ | 152 | __hash__ = object.__hash__ |
126 | 152 | 153 | ||
127 | 153 | # For get_obj_info(), an ObjectInfo is its own obj_info. | 154 | # For get_obj_info(), an ObjectInfo is its own obj_info. |
129 | 154 | __storm_object_info__ = property(lambda self:self) | 155 | __storm_object_info__ = property(lambda self: self) |
130 | 155 | 156 | ||
131 | 156 | def __init__(self, obj): | 157 | def __init__(self, obj): |
132 | 157 | # FASTPATH This method is part of the fast path. Be careful when | 158 | # FASTPATH This method is part of the fast path. Be careful when |
133 | @@ -171,7 +172,7 @@ | |||
134 | 171 | column.variable_factory(column=column, | 172 | column.variable_factory(column=column, |
135 | 172 | event=event, | 173 | event=event, |
136 | 173 | validator_object_factory=self.get_obj) | 174 | validator_object_factory=self.get_obj) |
138 | 174 | 175 | ||
139 | 175 | self.primary_vars = tuple(variables[column] | 176 | self.primary_vars = tuple(variables[column] |
140 | 176 | for column in self.cls_info.primary_key) | 177 | for column in self.cls_info.primary_key) |
141 | 177 | 178 | ||
142 | @@ -199,7 +200,6 @@ | |||
143 | 199 | from storm.cextensions import ObjectInfo, get_obj_info | 200 | from storm.cextensions import ObjectInfo, get_obj_info |
144 | 200 | 201 | ||
145 | 201 | 202 | ||
146 | 202 | |||
147 | 203 | class ClassAlias(object): | 203 | class ClassAlias(object): |
148 | 204 | """Create a named alias for a Storm class for use in queries. | 204 | """Create a named alias for a Storm class for use in queries. |
149 | 205 | 205 | ||
150 | @@ -229,8 +229,7 @@ | |||
151 | 229 | cls._storm_alias_cache = {} | 229 | cls._storm_alias_cache = {} |
152 | 230 | elif name in cache: | 230 | elif name in cache: |
153 | 231 | return cache[name] | 231 | return cache[name] |
156 | 232 | cls_info = get_cls_info(cls) | 232 | alias_cls = type(cls.__name__ + "Alias", (self_cls,), |
155 | 233 | alias_cls = type(cls.__name__+"Alias", (self_cls,), | ||
157 | 234 | {"__storm_table__": name}) | 233 | {"__storm_table__": name}) |
158 | 235 | alias_cls.__bases__ = (cls, self_cls) | 234 | alias_cls.__bases__ = (cls, self_cls) |
159 | 236 | alias_cls_info = get_cls_info(alias_cls) | 235 | alias_cls_info = get_cls_info(alias_cls) |
160 | 237 | 236 | ||
161 | === modified file 'tests/expr.py' | |||
162 | --- tests/expr.py 2011-05-11 05:38:41 +0000 | |||
163 | +++ tests/expr.py 2011-08-24 11:55:39 +0000 | |||
164 | @@ -135,6 +135,7 @@ | |||
165 | 135 | expr = Column() | 135 | expr = Column() |
166 | 136 | self.assertEquals(expr.name, Undef) | 136 | self.assertEquals(expr.name, Undef) |
167 | 137 | self.assertEquals(expr.table, Undef) | 137 | self.assertEquals(expr.table, Undef) |
168 | 138 | self.assertIdentical(expr.compile_cache, None) | ||
169 | 138 | 139 | ||
170 | 139 | # Test for identity. We don't want False there. | 140 | # Test for identity. We don't want False there. |
171 | 140 | self.assertTrue(expr.primary is 0) | 141 | self.assertTrue(expr.primary is 0) |
172 | @@ -1007,13 +1008,16 @@ | |||
173 | 1007 | statement = compile(expr, state) | 1008 | statement = compile(expr, state) |
174 | 1008 | self.assertEquals(statement, "column1") | 1009 | self.assertEquals(statement, "column1") |
175 | 1009 | self.assertEquals(state.parameters, []) | 1010 | self.assertEquals(state.parameters, []) |
176 | 1011 | self.assertEquals(expr.compile_cache, "column1") | ||
177 | 1010 | 1012 | ||
178 | 1011 | def test_column_table(self): | 1013 | def test_column_table(self): |
180 | 1012 | expr = Select(Column(column1, Func1())) | 1014 | column = Column(column1, Func1()) |
181 | 1015 | expr = Select(column) | ||
182 | 1013 | state = State() | 1016 | state = State() |
183 | 1014 | statement = compile(expr, state) | 1017 | statement = compile(expr, state) |
184 | 1015 | self.assertEquals(statement, "SELECT func1().column1 FROM func1()") | 1018 | self.assertEquals(statement, "SELECT func1().column1 FROM func1()") |
185 | 1016 | self.assertEquals(state.parameters, []) | 1019 | self.assertEquals(state.parameters, []) |
186 | 1020 | self.assertEquals(column.compile_cache, "column1") | ||
187 | 1017 | 1021 | ||
188 | 1018 | def test_column_contexts(self): | 1022 | def test_column_contexts(self): |
189 | 1019 | table, = track_contexts(1) | 1023 | table, = track_contexts(1) |
190 | @@ -1579,10 +1583,12 @@ | |||
191 | 1579 | 1583 | ||
192 | 1580 | def test_table(self): | 1584 | def test_table(self): |
193 | 1581 | expr = Table(table1) | 1585 | expr = Table(table1) |
194 | 1586 | self.assertIdentical(expr.compile_cache, None) | ||
195 | 1582 | state = State() | 1587 | state = State() |
196 | 1583 | statement = compile(expr, state) | 1588 | statement = compile(expr, state) |
197 | 1584 | self.assertEquals(statement, '"table 1"') | 1589 | self.assertEquals(statement, '"table 1"') |
198 | 1585 | self.assertEquals(state.parameters, []) | 1590 | self.assertEquals(state.parameters, []) |
199 | 1591 | self.assertEquals(expr.compile_cache, '"table 1"') | ||
200 | 1586 | 1592 | ||
201 | 1587 | def test_alias(self): | 1593 | def test_alias(self): |
202 | 1588 | expr = Alias(Table(table1), "name") | 1594 | expr = Alias(Table(table1), "name") |
203 | 1589 | 1595 | ||
204 | === modified file 'tests/info.py' | |||
205 | --- tests/info.py 2008-06-18 17:28:37 +0000 | |||
206 | +++ tests/info.py 2011-08-24 11:55:39 +0000 | |||
207 | @@ -94,7 +94,7 @@ | |||
208 | 94 | (self.Class.prop1, self.Class.prop2)) | 94 | (self.Class.prop1, self.Class.prop2)) |
209 | 95 | 95 | ||
210 | 96 | def test_table(self): | 96 | def test_table(self): |
212 | 97 | self.assertEquals(self.cls_info.table, "table") | 97 | self.assertEquals(self.cls_info.table.name, "table") |
213 | 98 | 98 | ||
214 | 99 | def test_primary_key(self): | 99 | def test_primary_key(self): |
215 | 100 | # Can't use == for props. | 100 | # Can't use == for props. |
216 | @@ -202,7 +202,7 @@ | |||
217 | 202 | 202 | ||
218 | 203 | def test_variables(self): | 203 | def test_variables(self): |
219 | 204 | self.assertTrue(isinstance(self.obj_info.variables, dict)) | 204 | self.assertTrue(isinstance(self.obj_info.variables, dict)) |
221 | 205 | 205 | ||
222 | 206 | for column in self.cls_info.columns: | 206 | for column in self.cls_info.columns: |
223 | 207 | variable = self.obj_info.variables.get(column) | 207 | variable = self.obj_info.variables.get(column) |
224 | 208 | self.assertTrue(isinstance(variable, Variable)) | 208 | self.assertTrue(isinstance(variable, Variable)) |
225 | @@ -227,7 +227,7 @@ | |||
226 | 227 | 227 | ||
227 | 228 | def test_primary_vars(self): | 228 | def test_primary_vars(self): |
228 | 229 | self.assertTrue(isinstance(self.obj_info.primary_vars, tuple)) | 229 | self.assertTrue(isinstance(self.obj_info.primary_vars, tuple)) |
230 | 230 | 230 | ||
231 | 231 | for column, variable in zip(self.cls_info.primary_key, | 231 | for column, variable in zip(self.cls_info.primary_key, |
232 | 232 | self.obj_info.primary_vars): | 232 | self.obj_info.primary_vars): |
233 | 233 | self.assertEquals(self.obj_info.variables.get(column), | 233 | self.assertEquals(self.obj_info.variables.get(column), |
234 | @@ -316,7 +316,7 @@ | |||
235 | 316 | self.obj_info.checkpoint() | 316 | self.obj_info.checkpoint() |
236 | 317 | 317 | ||
237 | 318 | obj = object() | 318 | obj = object() |
239 | 319 | 319 | ||
240 | 320 | self.obj_info.event.hook("changed", object_changed1, obj) | 320 | self.obj_info.event.hook("changed", object_changed1, obj) |
241 | 321 | self.obj_info.event.hook("changed", object_changed2, obj) | 321 | self.obj_info.event.hook("changed", object_changed2, obj) |
242 | 322 | 322 | ||
243 | @@ -326,7 +326,7 @@ | |||
244 | 326 | self.assertEquals(changes1, | 326 | self.assertEquals(changes1, |
245 | 327 | [(1, self.obj_info, self.variable2, Undef, 10, False, obj), | 327 | [(1, self.obj_info, self.variable2, Undef, 10, False, obj), |
246 | 328 | (1, self.obj_info, self.variable1, Undef, 20, False, obj)]) | 328 | (1, self.obj_info, self.variable1, Undef, 20, False, obj)]) |
248 | 329 | self.assertEquals(changes2, | 329 | self.assertEquals(changes2, |
249 | 330 | [(2, self.obj_info, self.variable2, Undef, 10, False, obj), | 330 | [(2, self.obj_info, self.variable2, Undef, 10, False, obj), |
250 | 331 | (2, self.obj_info, self.variable1, Undef, 20, False, obj)]) | 331 | (2, self.obj_info, self.variable1, Undef, 20, False, obj)]) |
251 | 332 | 332 | ||
252 | @@ -339,7 +339,7 @@ | |||
253 | 339 | self.assertEquals(changes1, | 339 | self.assertEquals(changes1, |
254 | 340 | [(1, self.obj_info, self.variable1, 20, None, False, obj), | 340 | [(1, self.obj_info, self.variable1, 20, None, False, obj), |
255 | 341 | (1, self.obj_info, self.variable2, 10, None, False, obj)]) | 341 | (1, self.obj_info, self.variable2, 10, None, False, obj)]) |
257 | 342 | self.assertEquals(changes2, | 342 | self.assertEquals(changes2, |
258 | 343 | [(2, self.obj_info, self.variable1, 20, None, False, obj), | 343 | [(2, self.obj_info, self.variable1, 20, None, False, obj), |
259 | 344 | (2, self.obj_info, self.variable2, 10, None, False, obj)]) | 344 | (2, self.obj_info, self.variable2, 10, None, False, obj)]) |
260 | 345 | 345 | ||
261 | @@ -352,7 +352,7 @@ | |||
262 | 352 | self.assertEquals(changes1, | 352 | self.assertEquals(changes1, |
263 | 353 | [(1, self.obj_info, self.variable1, None, Undef, False, obj), | 353 | [(1, self.obj_info, self.variable1, None, Undef, False, obj), |
264 | 354 | (1, self.obj_info, self.variable2, None, Undef, False, obj)]) | 354 | (1, self.obj_info, self.variable2, None, Undef, False, obj)]) |
266 | 355 | self.assertEquals(changes2, | 355 | self.assertEquals(changes2, |
267 | 356 | [(2, self.obj_info, self.variable1, None, Undef, False, obj), | 356 | [(2, self.obj_info, self.variable1, None, Undef, False, obj), |
268 | 357 | (2, self.obj_info, self.variable2, None, Undef, False, obj)]) | 357 | (2, self.obj_info, self.variable2, None, Undef, False, obj)]) |
269 | 358 | 358 | ||
270 | @@ -528,11 +528,11 @@ | |||
271 | 528 | prop1 = Property("column1", primary=True) | 528 | prop1 = Property("column1", primary=True) |
272 | 529 | self.Class = Class | 529 | self.Class = Class |
273 | 530 | self.ClassAlias = ClassAlias(self.Class, "alias") | 530 | self.ClassAlias = ClassAlias(self.Class, "alias") |
275 | 531 | 531 | ||
276 | 532 | def test_cls_info_cls(self): | 532 | def test_cls_info_cls(self): |
277 | 533 | cls_info = get_cls_info(self.ClassAlias) | 533 | cls_info = get_cls_info(self.ClassAlias) |
278 | 534 | self.assertEquals(cls_info.cls, self.Class) | 534 | self.assertEquals(cls_info.cls, self.Class) |
280 | 535 | self.assertEquals(cls_info.table, "alias") | 535 | self.assertEquals(cls_info.table.name, "alias") |
281 | 536 | self.assertEquals(self.ClassAlias.prop1.name, "column1") | 536 | self.assertEquals(self.ClassAlias.prop1.name, "column1") |
282 | 537 | self.assertEquals(self.ClassAlias.prop1.table, self.ClassAlias) | 537 | self.assertEquals(self.ClassAlias.prop1.table, self.ClassAlias) |
283 | 538 | 538 |
Nice!
Would you mind to repeat the benchmark with 1000 inserts and 10 columns each, to see a bit better the effects? The first number feels a bit suspect (hard to believe 10 inserts would take 1.6 seconds).