[Pharo-project] Performance Tip: Using buffered streams
Hi, <short-version> When doing complex IO, like writing or parsing protocols, to non-memory streams, like file streams (and probably socket streams), buffering can make an important difference. ZnBufferedWriteStream and ZnBufferedReadStream are an easy solution to experience the difference. </short-version> And now the long version: first some measurements (code based on latest versions of Zinc, NeoCSV, NeoJSON and STON): NeoJSON | data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.json' asFileReference writeStreamDo: [ :stream | (NeoJSONWriter on: stream) nextPut: data ] ] timeToRun. 22152 | data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.json' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | (NeoJSONWriter on: stream) nextPut: data ] ] ] timeToRun. 5147 [ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: stream) next ] ] timeToRun. 3502 [ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1214 STON | data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.ston' asFileReference writeStreamDo: [ :stream | STON writer on: stream; nextPut: data ] ] timeToRun. 22275 | data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.ston' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | STON writer on: stream; nextPut: data ] ] ] timeToRun. 5501 [ '/tmp/numbers.ston' asFileReference readStreamDo: [ :stream | STON reader on: stream; next ] ] timeToRun. 3632 [ '/tmp/numbers.ston' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | STON reader on: stream; next ] ] ] timeToRun. 1367 NeoCSV | data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.csv' asFileReference writeStreamDo: [ :stream | (NeoCSVWriter on: stream) nextPutAll: data ] ] timeToRun. 20916 | data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.csv' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | (NeoCSVWriter on: stream) nextPutAll: data ] ] ] timeToRun. 5559 [ '/tmp/numbers.csv' asFileReference readStreamDo: [ :stream | (NeoCSVReader on: stream) addIntegerField; addIntegerField; addFloatField; addFloatField; addField; upToEnd ] ] timeToRun. 1290 [ '/tmp/numbers.csv' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoCSVReader on: stream) addIntegerField; addIntegerField; addFloatField; addFloatField; addField; upToEnd ] ] ] timeToRun. 970 The reason for these non-trivial speedups is that buffering is missing and/or buffer management is suboptimal in the standard file stream classes. Simply wrapping the stream with a 64Kb buffer yields results like this. We already knew this for writing (and Fuel adapted it as well), but it seems to make a similar, although smaller difference when reading. <side-note> Attentive, curious readers might ask why in this particular benchmark the reading is so much slower than the writing - normally it should be the inverse or at least they should be closer to each other. Lo and behold: another performance issue: float printing (as opposed to parsing) is extremely slow (probably partially due to its reliance on LargeInteger arithmetic) ! Here is a similar benchmark with only Integers: | data | data := (1 to: 25000) collect: [ :each | { each. each negated. each + 100000. each - 100000. each asWords } ]. [ '/tmp/numbers.csv' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | (NeoCSVWriter on: stream) nextPutAll: data ] ] ] timeToRun. 311 [ '/tmp/numbers.csv' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoCSVReader on: stream) addIntegerField; addIntegerField; addIntegerField; addIntegerField; addField; upToEnd ] ] ] timeToRun. 396 </side-note> If ZnBufferedWriteStream or ZnBufferedReadStream were useful in speading up your code, please let us know. Sven PS: There is one ZnBufferedReadStream limitation: by design, #position, #position: or #skip: -1 and everything on top of that are NOT supported - IMHO real streams should not (and cannot) provide these operations. A sad consequence is that NumberParser and its subclasses (and thus Number class>>#readFrom:) do not work. NeoJSON and STON do their own number parsing, NeoCSV relies on a new class, NeoNumberParser, that is independently usable, though restricted to normal numbers as opposed to the full Smalltalk syntax. -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
On Sun, Dec 2, 2012 at 11:09 PM, Sven Van Caekenberghe <sven@stfx.eu> wrote:
Hi,
<short-version>
When doing complex IO, like writing or parsing protocols, to non-memory streams, like file streams (and probably socket streams), buffering can make an important difference. ZnBufferedWriteStream and ZnBufferedReadStream are an easy solution to experience the difference.
Yes, I noticed that in Fuel and that's why we use the buffered write stream always. It was a huge difference.
</short-version>
And now the long version: first some measurements (code based on latest versions of Zinc, NeoCSV, NeoJSON and STON):
NeoJSON
| data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.json' asFileReference writeStreamDo: [ :stream | (NeoJSONWriter on: stream) nextPut: data ] ] timeToRun. 22152
| data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.json' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | (NeoJSONWriter on: stream) nextPut: data ] ] ] timeToRun. 5147
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: stream) next ] ] timeToRun. 3502
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1214
STON
| data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.ston' asFileReference writeStreamDo: [ :stream | STON writer on: stream; nextPut: data ] ] timeToRun. 22275
| data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.ston' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | STON writer on: stream; nextPut: data ] ] ] timeToRun. 5501
[ '/tmp/numbers.ston' asFileReference readStreamDo: [ :stream | STON reader on: stream; next ] ] timeToRun. 3632
[ '/tmp/numbers.ston' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | STON reader on: stream; next ] ] ] timeToRun. 1367
NeoCSV
| data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.csv' asFileReference writeStreamDo: [ :stream | (NeoCSVWriter on: stream) nextPutAll: data ] ] timeToRun. 20916
| data | data := (1 to: 25000) collect: [ :each | { each. each negated. each reciprocal asFloat. each sqrt. each asWords } ]. [ '/tmp/numbers.csv' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | (NeoCSVWriter on: stream) nextPutAll: data ] ] ] timeToRun. 5559
[ '/tmp/numbers.csv' asFileReference readStreamDo: [ :stream | (NeoCSVReader on: stream) addIntegerField; addIntegerField; addFloatField; addFloatField; addField; upToEnd ] ] timeToRun. 1290
[ '/tmp/numbers.csv' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoCSVReader on: stream) addIntegerField; addIntegerField; addFloatField; addFloatField; addField; upToEnd ] ] ] timeToRun. 970
The reason for these non-trivial speedups is that buffering is missing and/or buffer management is suboptimal in the standard file stream classes. Simply wrapping the stream with a 64Kb buffer yields results like this. We already knew this for writing (and Fuel adapted it as well), but it seems to make a similar, although smaller difference when reading.
This is really really interesting, because it means a speed up of 2x when reading :) Where can I get the last version of ZnBufferedReadStream?
<side-note>
Attentive, curious readers might ask why in this particular benchmark the reading is so much slower than the writing - normally it should be the inverse or at least they should be closer to each other. Lo and behold: another performance issue: float printing (as opposed to parsing) is extremely slow (probably partially due to its reliance on LargeInteger arithmetic) !
I remember Nicolas Cieller doing something about Floats and printing...maybe he improved something?
Here is a similar benchmark with only Integers:
| data | data := (1 to: 25000) collect: [ :each | { each. each negated. each + 100000. each - 100000. each asWords } ]. [ '/tmp/numbers.csv' asFileReference writeStreamDo: [ :fstream | ZnBufferedWriteStream on: fstream do: [ :stream | (NeoCSVWriter on: stream) nextPutAll: data ] ] ] timeToRun. 311
[ '/tmp/numbers.csv' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoCSVReader on: stream) addIntegerField; addIntegerField; addIntegerField; addIntegerField; addField; upToEnd ] ] ] timeToRun. 396
</side-note>
If ZnBufferedWriteStream or ZnBufferedReadStream were useful in speading up your code, please let us know.
Sven
PS: There is one ZnBufferedReadStream limitation: by design, #position, #position: or #skip: -1 and everything on top of that are NOT supported - IMHO real streams should not (and cannot) provide these operations. A sad consequence is that NumberParser and its subclasses (and thus Number class>>#readFrom:) do not work. NeoJSON and STON do their own number parsing, NeoCSV relies on a new class, NeoNumberParser, that is independently usable, though restricted to normal numbers as opposed to the full Smalltalk syntax.
I want to give it a try for Fuel.
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
-- Mariano http://marianopeck.wordpress.com
Hi Mariano, On 02 Dec 2012, at 23:43, Mariano Martinez Peck <marianopeck@gmail.com> wrote:
This is really really interesting, because it means a speed up of 2x when reading :) Where can I get the last version of ZnBufferedReadStream?
I knew you would be interested: it is in the latest Zn, either http://mc.stfx.eu or squeaksource (not in the metacello yet). It should work on binary streams too, I don't know whether it would make a lot of difference, I would guess so. Maybe Fuel uses lots of 'deterministic reading' (size prefixed) instead of parsing. Sven -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
Sven Van Caekenberghe-2 wrote
Hi Mariano,
On 02 Dec 2012, at 23:43, Mariano Martinez Peck <
marianopeck@
> wrote:
This is really really interesting, because it means a speed up of 2x when reading :) Where can I get the last version of ZnBufferedReadStream?
I knew you would be interested: it is in the latest Zn, either http://mc.stfx.eu or squeaksource (not in the metacello yet). It should work on binary streams too, I don't know whether it would make a lot of difference, I would guess so. Maybe Fuel uses lots of 'deterministic reading' (size prefixed) instead of parsing.
Sven
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
Did you (or anybody else for that matter) tried to use persistent arrays as base for in memory streeams? They are basically shallow tree that provides array like access. Their claim to fame is that one can avoid copy operation when array gets resized. So it _might_ be usable when one handles large write streams and does not know in advance how large they will get. ----- http://www.cloud208.com/ -- View this message in context: http://forum.world.st/Performance-Tip-Using-buffered-streams-tp4657739p46577... Sent from the Pharo Smalltalk mailing list archive at Nabble.com.
On 03 Dec 2012, at 07:25, Sven Van Caekenberghe <sven@stfx.eu> wrote:
Hi Mariano,
On 02 Dec 2012, at 23:43, Mariano Martinez Peck <marianopeck@gmail.com> wrote:
This is really really interesting, because it means a speed up of 2x when reading :) Where can I get the last version of ZnBufferedReadStream?
I knew you would be interested: it is in the latest Zn, either http://mc.stfx.eu or squeaksource (not in the metacello yet). It should work on binary streams too, I don't know whether it would make a lot of difference, I would guess so. Maybe Fuel uses lots of 'deterministic reading' (size prefixed) instead of parsing.
Well, I just tried. The good news is, it works, the bad news is, it does not make a difference, either way: | data | data := STONTestMap classTreeExtended. [ FLSerializer serialize: data toFileNamed: '/tmp/all.fuel' ] timeToRun. 4967 [ FLMaterializer materializeFromFileNamed: '/tmp/all.fuel' ] timeToRun. 1095 [ '/tmp/all.fuel' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream binary do: [ :stream | FLMaterializer newDefault materializeFrom: stream ] ] ] timeToRun. 1043 So I guess Fuel is already pretty optimised, especially for reading ;-) I think ZnBufferedReadStream only makes a difference for parsers that are sending lots of little #next, #peek and #atEnd messages in loops. Binary protocols are typically more deterministic, they know ahead what they have to read and don't have to do #peek or #atEnd tests all the time. Sven -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
On 03.12.2012 09:30, Sven Van Caekenberghe wrote:
On 03 Dec 2012, at 07:25, Sven Van Caekenberghe <sven@stfx.eu> wrote:
Hi Mariano,
On 02 Dec 2012, at 23:43, Mariano Martinez Peck <marianopeck@gmail.com> wrote:
This is really really interesting, because it means a speed up of 2x when reading :) Where can I get the last version of ZnBufferedReadStream? I knew you would be interested: it is in the latest Zn, either http://mc.stfx.eu or squeaksource (not in the metacello yet). It should work on binary streams too, I don't know whether it would make a lot of difference, I would guess so. Maybe Fuel uses lots of 'deterministic reading' (size prefixed) instead of parsing. Well, I just tried. The good news is, it works, the bad news is, it does not make a difference, either way:
| data | data := STONTestMap classTreeExtended. [ FLSerializer serialize: data toFileNamed: '/tmp/all.fuel' ] timeToRun. 4967
[ FLMaterializer materializeFromFileNamed: '/tmp/all.fuel' ] timeToRun. 1095
[ '/tmp/all.fuel' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream binary do: [ :stream | FLMaterializer newDefault materializeFrom: stream ] ] ] timeToRun. 1043
So I guess Fuel is already pretty optimised, especially for reading ;-)
I think ZnBufferedReadStream only makes a difference for parsers that are sending lots of little #next, #peek and #atEnd messages in loops. Binary protocols are typically more deterministic, they know ahead what they have to read and don't have to do #peek or #atEnd tests all the time.
Sven
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill The reduced read times you are seeing, is lack of decoding, not lack of buffering.
BufferedReadStream uses readInto:startingAt:count:, which isn't reimplemented for MultiByteFileStream to do proper decoding, and thus inherits the one from StandardFileStream. The filestreams already do read buffering (albeit with a smaller buffer size), so when Fuel uses binary mode (which skips decoding), the performance is pretty much the same. Cheers, Henry
On 03.12.2012 12:10, Henrik Sperre Johansen wrote:
On 03.12.2012 09:30, Sven Van Caekenberghe wrote:
On 03 Dec 2012, at 07:25, Sven Van Caekenberghe <sven@stfx.eu> wrote:
Hi Mariano,
On 02 Dec 2012, at 23:43, Mariano Martinez Peck <marianopeck@gmail.com> wrote:
This is really really interesting, because it means a speed up of 2x when reading :) Where can I get the last version of ZnBufferedReadStream? I knew you would be interested: it is in the latest Zn, either http://mc.stfx.eu or squeaksource (not in the metacello yet). It should work on binary streams too, I don't know whether it would make a lot of difference, I would guess so. Maybe Fuel uses lots of 'deterministic reading' (size prefixed) instead of parsing. Well, I just tried. The good news is, it works, the bad news is, it does not make a difference, either way:
| data | data := STONTestMap classTreeExtended. [ FLSerializer serialize: data toFileNamed: '/tmp/all.fuel' ] timeToRun. 4967
[ FLMaterializer materializeFromFileNamed: '/tmp/all.fuel' ] timeToRun. 1095
[ '/tmp/all.fuel' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream binary do: [ :stream | FLMaterializer newDefault materializeFrom: stream ] ] ] timeToRun. 1043
So I guess Fuel is already pretty optimised, especially for reading ;-)
I think ZnBufferedReadStream only makes a difference for parsers that are sending lots of little #next, #peek and #atEnd messages in loops. Binary protocols are typically more deterministic, they know ahead what they have to read and don't have to do #peek or #atEnd tests all the time.
Sven
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill The reduced read times you are seeing, is lack of decoding, not lack of buffering.
BufferedReadStream uses readInto:startingAt:count:, which isn't reimplemented for MultiByteFileStream to do proper decoding, and thus inherits the one from StandardFileStream.
The filestreams already do read buffering (albeit with a smaller buffer size), so when Fuel uses binary mode (which skips decoding), the performance is pretty much the same.
Cheers, Henry In other words, to get comparable runtimes, you'd have to include a ZnEncodedStream doing UTF-8 decoding on top of the stack used by the STON-reader.
(At which point you're well on the way to reimplementing the modular streams of Xtreams, but with a more traditional API :) ) Cheers, Henry
On 03 Dec 2012, at 12:22, Henrik Sperre Johansen <henrik.s.johansen@veloxit.no> wrote:
In other words, to get comparable runtimes, you'd have to include a ZnEncodedStream doing UTF-8 decoding on top of the stack used by the STON-reader.
There are ZnCharacterReadStream and ZnCharacterWriteStream that add decoding/encoding to binary streams. They could indeed be used as an alternative. But that was not my point.
(At which point you're well on the way to reimplementing the modular streams of Xtreams, but with a more traditional API :) )
Many people, including myself are interested in Xtreams - many fun things will happen in the future ;-) -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
On 03 Dec 2012, at 12:43, Sven Van Caekenberghe <sven@stfx.eu> wrote:
There are ZnCharacterReadStream and ZnCharacterWriteStream that add decoding/encoding to binary streams. They could indeed be used as an alternative. But that was not my point.
I reran the examples using a binary file stream to disable the decoding and then using ZnCharacterReadStream to handle the decoding: [ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: stream) next ] ] timeToRun. 3502 [ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1214 [ StandardFileStream fileNamed: '/tmp/numbers.json' do: [ :stream | (NeoJSONReader on: stream) next ] ] timeToRun 2869 [ StandardFileStream fileNamed: '/tmp/numbers.json' do: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun 1280 [ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: (ZnCharacterReadStream on: stream binary)) next ] ] timeToRun. 1655 [ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: (ZnCharacterReadStream on: stream binary encoding: 'latin1')) next ] ] timeToRun. 1574 [ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: (ZnCharacterReadStream on: fstream binary) do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1753 [ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: (ZnCharacterReadStream on: fstream binary encoding: 'latin1') do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1655 This shows several things (for this particular test case): - ZnCharacterReadStream is at least as good as the builtin decoding streams (here twice as good) - UTF-8 (ZnUTF8Encoder) and Latin1 (ZnNullEncoder) decoding are close together (because the input is ASCII) - ZnBufferedReadStream does not help when the stream is pretty good (ZnCharacterReadStream actually uses a 1 character buffer to support peek, which is important to speed up this use case) - this is to be expected: it adds a layer, but its fetch next buffer logic still loops using #next, decoding possibly Unicode characters - the super fast runs (the 2nd and the 4th) are actually cheating because they skip decoding - thanks for that observation ! Sven PS: While doing these explorations, I actually updated ZnUTF8Encoder quite a bit, both when handling ASCII, which is the majority of characters in most cases, as well as in the general case; see the latest version. -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
Yes, the lack of FileStream buffering was (re)-discovered and corrected when I have experimented with my own SqueaXTream experiments http://www.squeaksource.com/XTream/ before porting the original Xtreams to Squeak http://www.squeaksource.com/Xtreams/ Then Levente soon came up with an excellent patch for buffering Squeak FIleStream and this was later ported to Pharo. What I also re-discovered is that not only buffering the raw file counts, but also buffering the decoding. Indded, an uncarefull implementation would read file by chunk but decode byte after byte... While a buffered one would decode large chunks of ASCII (if any) in a single copy primitive. This was reported near the end of this post: http://permalink.gmane.org/gmane.comp.lang.smalltalk.squeak.general/151341 For Xtreams, I don't remember exactly where I put some experiments, but pre-allocating a WideString instead of a String can also make a noticeable difference in presence of WideCharacter... Nicolas 2012/12/3 Sven Van Caekenberghe <sven@stfx.eu>:
On 03 Dec 2012, at 12:43, Sven Van Caekenberghe <sven@stfx.eu> wrote:
There are ZnCharacterReadStream and ZnCharacterWriteStream that add decoding/encoding to binary streams. They could indeed be used as an alternative. But that was not my point.
I reran the examples using a binary file stream to disable the decoding and then using ZnCharacterReadStream to handle the decoding:
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: stream) next ] ] timeToRun. 3502
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1214
[ StandardFileStream fileNamed: '/tmp/numbers.json' do: [ :stream | (NeoJSONReader on: stream) next ] ] timeToRun 2869
[ StandardFileStream fileNamed: '/tmp/numbers.json' do: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun 1280
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: (ZnCharacterReadStream on: stream binary)) next ] ] timeToRun. 1655
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :stream | (NeoJSONReader on: (ZnCharacterReadStream on: stream binary encoding: 'latin1')) next ] ] timeToRun. 1574
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: (ZnCharacterReadStream on: fstream binary) do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1753
[ '/tmp/numbers.json' asFileReference readStreamDo: [ :fstream | ZnBufferedReadStream on: (ZnCharacterReadStream on: fstream binary encoding: 'latin1') do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun. 1655
This shows several things (for this particular test case):
- ZnCharacterReadStream is at least as good as the builtin decoding streams (here twice as good) - UTF-8 (ZnUTF8Encoder) and Latin1 (ZnNullEncoder) decoding are close together (because the input is ASCII) - ZnBufferedReadStream does not help when the stream is pretty good (ZnCharacterReadStream actually uses a 1 character buffer to support peek, which is important to speed up this use case) - this is to be expected: it adds a layer, but its fetch next buffer logic still loops using #next, decoding possibly Unicode characters - the super fast runs (the 2nd and the 4th) are actually cheating because they skip decoding - thanks for that observation !
Sven
PS: While doing these explorations, I actually updated ZnUTF8Encoder quite a bit, both when handling ASCII, which is the majority of characters in most cases, as well as in the general case; see the latest version.
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
On 03 Dec 2012, at 19:24, Nicolas Cellier <nicolas.cellier.aka.nice@gmail.com> wrote:
Yes, the lack of FileStream buffering was (re)-discovered and corrected when I have experimented with my own SqueaXTream experiments http://www.squeaksource.com/XTream/ before porting the original Xtreams to Squeak http://www.squeaksource.com/Xtreams/ Then Levente soon came up with an excellent patch for buffering Squeak FIleStream and this was later ported to Pharo.
What I also re-discovered is that not only buffering the raw file counts, but also buffering the decoding. Indded, an uncarefull implementation would read file by chunk but decode byte after byte... While a buffered one would decode large chunks of ASCII (if any) in a single copy primitive.
This was reported near the end of this post: http://permalink.gmane.org/gmane.comp.lang.smalltalk.squeak.general/151341
For Xtreams, I don't remember exactly where I put some experiments, but pre-allocating a WideString instead of a String can also make a noticeable difference in presence of WideCharacter...
Many people seem to be rediscovering the same problems, not a good sign ;-) I think that in particular MultiByteFileStream>>#peek is not efficient. Yeah, there seems to be a WideString construction issue as well, I haven't looked into it yet. -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
one day we will have to address the old streams :) On Dec 3, 2012, at 9:44 PM, Sven Van Caekenberghe wrote:
On 03 Dec 2012, at 19:24, Nicolas Cellier <nicolas.cellier.aka.nice@gmail.com> wrote:
Yes, the lack of FileStream buffering was (re)-discovered and corrected when I have experimented with my own SqueaXTream experiments http://www.squeaksource.com/XTream/ before porting the original Xtreams to Squeak http://www.squeaksource.com/Xtreams/ Then Levente soon came up with an excellent patch for buffering Squeak FIleStream and this was later ported to Pharo.
What I also re-discovered is that not only buffering the raw file counts, but also buffering the decoding. Indded, an uncarefull implementation would read file by chunk but decode byte after byte... While a buffered one would decode large chunks of ASCII (if any) in a single copy primitive.
This was reported near the end of this post: http://permalink.gmane.org/gmane.comp.lang.smalltalk.squeak.general/151341
For Xtreams, I don't remember exactly where I put some experiments, but pre-allocating a WideString instead of a String can also make a noticeable difference in presence of WideCharacter...
Many people seem to be rediscovering the same problems, not a good sign ;-)
I think that in particular MultiByteFileStream>>#peek is not efficient.
Yeah, there seems to be a WideString construction issue as well, I haven't looked into it yet.
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
=> Xtreams and Pharo 3.0 ;) I think anything else is a major waste of time :P On 2012-12-05, at 14:53, Stéphane Ducasse <stephane.ducasse@inria.fr> wrote:
one day we will have to address the old streams :)
On Dec 3, 2012, at 9:44 PM, Sven Van Caekenberghe wrote:
On 03 Dec 2012, at 19:24, Nicolas Cellier <nicolas.cellier.aka.nice@gmail.com> wrote:
Yes, the lack of FileStream buffering was (re)-discovered and corrected when I have experimented with my own SqueaXTream experiments http://www.squeaksource.com/XTream/ before porting the original Xtreams to Squeak http://www.squeaksource.com/Xtreams/ Then Levente soon came up with an excellent patch for buffering Squeak FIleStream and this was later ported to Pharo.
What I also re-discovered is that not only buffering the raw file counts, but also buffering the decoding. Indded, an uncarefull implementation would read file by chunk but decode byte after byte... While a buffered one would decode large chunks of ASCII (if any) in a single copy primitive.
This was reported near the end of this post: http://permalink.gmane.org/gmane.comp.lang.smalltalk.squeak.general/151341
For Xtreams, I don't remember exactly where I put some experiments, but pre-allocating a WideString instead of a String can also make a noticeable difference in presence of WideCharacter...
Many people seem to be rediscovering the same problems, not a good sign ;-)
I think that in particular MultiByteFileStream>>#peek is not efficient.
Yeah, there seems to be a WideString construction issue as well, I haven't looked into it yet.
-- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
Hendrik, Thanks for entering this discussion, it is important territory, for many users. We should get the basics right. On 03 Dec 2012, at 12:10, Henrik Sperre Johansen <henrik.s.johansen@veloxit.no> wrote:
The reduced read times you are seeing, is lack of decoding, not lack of buffering.
I should have been more clear: it is a combination of buffering & decoding (see further).
BufferedReadStream uses readInto:startingAt:count:, which isn't reimplemented for MultiByteFileStream to do proper decoding, and thus inherits the one from StandardFileStream.
If that is so (and it seems to be the case), then that is an important bug, wouldn't you agree ?
The filestreams already do read buffering (albeit with a smaller buffer size), so when Fuel uses binary mode (which skips decoding), the performance is pretty much the same.
Yeah, in binary mode, MultiByteFilestream does not do encoding. Still, IMO that is not the only reason for the difference. However, even when I go straight to MultiByteFilestream's superclass, StandardFileStream, which does not do decoding, the additional buffering helps a lot: [ StandardFileStream fileNamed: '/tmp/numbers.json' do: [ :stream | (NeoJSONReader on: stream) next ] ] timeToRun 2869 [ StandardFileStream fileNamed: '/tmp/numbers.json' do: [ :fstream | ZnBufferedReadStream on: fstream do: [ :stream | (NeoJSONReader on: stream) next ] ] ] timeToRun 1280 I said from the start that the access pattern matters: parser code is a sweet spot here. Sven -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
On 03 Dec 2012, at 12:37, Sven Van Caekenberghe <sven@stfx.eu> wrote:
If that is so (and it seems to be the case), then that is an important bug, wouldn't you agree ?
http://code.google.com/p/pharo/issues/detail?id=7092 -- Sven Van Caekenberghe http://stfx.eu Smalltalk is the Red Pill
participants (7)
-
Camillo Bruni -
drush66 -
Henrik Sperre Johansen -
Mariano Martinez Peck -
Nicolas Cellier -
Stéphane Ducasse -
Sven Van Caekenberghe