[Development] Text streaming interface & function_view

Fawzi Mohamed Fawzi.Mohamed at qt.io
Mon Jun 15 08:11:21 CEST 2020


Ehm I realised that I did not explain how such a lightweight streaming interface would look like:
* function accepting a string -> function accepting a Dumpable: either a QStringView or a void(Sink) function, always behaving like a void(Sink function)
* function returning a string -> void(Sink) function (i.e. QString toString() -> void dump(Sink))
This makes sense only for functions immediately using the string passed in, but is lightweight to implement, and allows streaming in the result without creating temporary QStrings (it also can help when decoding on the fly).

Fawzi

> On 15. Jun 2020, at 01:27, Fawzi Mohamed <Fawzi.Mohamed at qt.io> wrote:
> 
> In the past I worked with the tango library with the D language, that did try to avoid heap allocations and in my own blip library I had build most of the text output on a Sink: a functor that accepts a string.
> For graphical user interfaces that is not an issue still I think that a Sink defined as
> ```
> using Sink = std::function(void(QStringView));
> ```
> can give a nice lightweight “text streaming” interface.
> I played a bit with it, but my goal wasn’t developing that part, but I wanted to come back and benchmark it to see if my feeling was correct.
> 
> I basically benchmarked 
> ```
> void writeQString(QTextStream &s, int repeat = 100) {
>    s << QLatin1String("start\n");
>    for (int i = 0; i < repeat; ++i) {
>        QString base = QStringLiteral(u"bla bla");
>        for (int j = 0; j < 3; ++j) {
>            s << base.repeated(j);
>            s << QStringLiteral(u"%1 bla %2 %3\n").arg(QStringLiteral(u"  ").repeated(j), base, base + base);
>        }
>    }
>    s << QLatin1String("end\n");
> }
> ```
> 
> 
> Against a version using a Sink
> ```
> void repeatOut(Sink sink, int nrepeat, std::function<void(Sink)> dumper) {
>    for (int i = 0; i < nrepeat; ++i)
>        dumper(sink);
> }
> 
> void writeDumper(QTextStream &s, int repeat = 100) {
>    Sink sink = [&s](QStringView str){ s << str; };
>    sink(u"start\n");
>    for (int i = 0; i < repeat; ++i) {
>        QStringView base = u"bla bla";
>        for (int j = 0; j < 3; ++j) {
>            repeatOut(sink, j, [base](Sink sink) { sink(base); });
>            [base, j](Sink sink) {
>                repeatOut(sink, j, [](Sink sink) { sink(u"  "); });
>                sink(u" bla ");
>                sink(base);
>                sink(u" ");
>                sink(base);
>                sink(base);
>                sink(u"\n");
>            }(sink);
>        }
>    }
>    sink(u"end\n");
> }
> ```
> What I learnt is that
> 
> 1) you have to be careful about comparing with String::repeated because adding a large string enlarges the cache of the QTextStram and makes comparison invalid (QString seems then much faster).
> 
> Not having understood point 1 I looked at passing all function objects as const & (which was faster), and finally using a non owing functor reference (like function_view discussed by Vittorio Romeo in https://vittorioromeo.info/index/blog/passing_functions_to_functions.html ), which was even faster (in D1 delegates were non owning).
> 
> The full source of my benchmark  is available at https://github.com/fawzi/qtIoTest and on my macBook 2.3 GHz i9 with flash storage I got the following numbers:
> 
> Debug
> "QStringAlloc" write: "485.847010" writeFlush: "485.879594" ioTime: "486.158937" flushFails: 0
> "LambdaAndSink" write: "987.612934" writeFlush: "987.640574" ioTime: "987.993028" flushFails: 0
> "LambdaAndSinkCRef" write: "843.016367" writeFlush: "843.030152" ioTime: "844.147218" flushFails: 0
> "LambdaAndSinkView" write: "483.614625" writeFlush: "483.630823" ioTime: "483.777161" flushFails: 0
> 
> Release
> "QStringAlloc" write: "305.894019" writeFlush: "305.924705" ioTime: "306.177800" flushFails: 0
> "LambdaAndSink" write: "102.757184" writeFlush: "102.768369" ioTime: "102.955962" flushFails: 0
> "LambdaAndSinkCRef" write: "91.224731" writeFlush: "91.232051" ioTime: "91.642540" flushFails: 0
> "LambdaAndSinkView" write: "79.895681" writeFlush: "79.915456" ioTime: "80.602022" flushFails: 0
> 
> In debug mode the function_view Is just as fast, but in release mode it is *much* faster, indeed all streaming options win over the plain String allocation.
> 
> So I have two points in my conclusion:
> 
> a) I think that it could make sense, if there is interest, to use Sink and void(Sink) functions (or function_view) more, and provide corresponding API (one can for example define a Dumpable that can be  instantiated with either a QStringView of a void(Sink) function, and thus have format(Sink, QStringView, Dumpable, Dumpable,…), or even a wrapper around a Sink that provides nicer stream like interface.
> Strict GUI programming might not gain much, but any string output would be a bit faster, and I think, cleaner.
> 
> b) Independently from the text streaming discussion, a non owning function pointer like function_view could be nice to have in Qt.
> It happens in several places that one wants to pass a function as argument that will be called immediately and not stored, for example as comparing function to sort.
> In these case function_view being lighter weight and higher performance might make the API even more attractive (any long term storing / full closure use still needs std::function).
> 
> Bonus note
> QTexCodec streaming use is ugly, QTextDecoder is a bit better, but by not exposing the existence of partially decoded code points in any way is still incomplete, fixing that if not already scheduled for Qt6 it should probably be added.
> _______________________________________________
> Development mailing list
> Development at qt-project.org
> https://lists.qt-project.org/listinfo/development



More information about the Development mailing list