0

I have a collection of QVariantMaps which contains QVariantLists in SOME of their entries.

I need to append to the lists. These lists can grow rather large, and I have to do this for many of them, many times per second, basically the whole time the program is running. So, efficiency is pretty important here..

I'm not arriving at a way to avoid making copies of these lists though! They might as well be immutable. :(

This works:

map[ key ] = QVariantList() << map[ key ].toList() << someOtherVarList;

But, that's doing a whole lot more work (under the hood) than I want...

Here's something I've tried, but ran into a wall:

I can get a non-const pointer to the variant I want to modify directly by using this macro I wrote utilizing a QMutableMapIterator:

#define getMapValuePtr( keyType, valueType, map, keyValue, ptrName ) \
    valueType *ptrName( nullptr ); \
    for( QMap<keyType, valueType>::iterator it = map.begin(); \
         it != map.end(); ++it ) \
        { if( it.key()==keyValue ) { ptrName = &(*it); break; } }

Example:

const QString key( "mylist" );
QVariantMap map;
map[ key ] = QVariantList( {"a","b","c"} );           
getMapValuePtr( QString, QVariant, map, key, valuePtr )

So now valuePtr could give me the ability to directly modify the value in the map, but the fact it's a QVariant is still in the way...

I have next tried the following without Qt Creator highlighting anything to indicate an error, but it does NOT quite compile despite that...

QVariantList *valListPtr( qvariant_cast<QVariantList *>(*valuePtr) );    
valListPtr->append( QVariantList( {"x","y","z"} ) );

Ideas?

Note: Not all entries in these maps are list. I CANNOT therefore change my map type to QMap<QString,QVariantList>. I tried, and the many client classes to this code all threw fits... That would have simplified matters, had it worked, by implicitly eliminating this final sticking point.

BuvinJ
  • 10,221
  • 5
  • 83
  • 96
  • Does `operator[]` make a copy? – JarMan Apr 07 '22 at 22:34
  • `map[ key ].toList()` makes a copy of the list stored inside the variant – BuvinJ Apr 07 '22 at 22:47
  • Actually, `map[ key ]` returns a copy of the variant, then `.toList()` returns a copy of the list nested inside... Additionally, the use of `map[ key ]` twice here leads to two lookups for the map entry. And remember when these temps are created - EVERY variant in the list gets its constructor and destructor fired off. And those all those list items, being variants, may themselves contain any number of nested members... – BuvinJ Apr 08 '22 at 13:20
  • 1
    If you care about efficiency, you should consider not using `QVariant` as the underlying data type, because it has quite a bit of overhead. – hyde Apr 22 '22 at 13:47
  • `map[key]` *doesn't copy*: [`T &QMap::operator[](const Key &key)`](https://doc.qt.io/qt-5/qmap.html#operator-5b-5d) – Caleth Apr 22 '22 at 14:09

2 Answers2

0

I have a collection of QVariantMaps which contains QVariantLists in SOME of their entries.

I need to append to the lists.

Don't. Obviously you have some schema to these QVariants, otherwise you wouldn't know if they are a list or not. Do you operations against a strongly typed model of that, then at the very last moment, transform that into a QVariant (or whatever)

map[ key ] = QVariantList() << map[ key ].toList() << someOtherVarList;

That's copying every element at least twice, and looking up the variant twice, so you can shave an immediate 50% by dropping it to

auto & var = map[ key ]; 
var = (var.toList() << someOtherVarList);

The main problem is that QVariant is garbage for performance. Contrast std::any which lets you have reference access to the stored value.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • In general, I would agree. Using a different data structure would often be a better approach. In my actual use case here, that would have been absurdly more work, however, and mean would redesigning a giant pile of classes simply to make this one function work faster. That wouldn't make sense. – BuvinJ Apr 22 '22 at 13:52
  • @BuvinJ maybe rather than spending all your time micro-optimising access to something inherantly slow, you could have written a not-slow by default thing in the first place – Caleth Apr 22 '22 at 13:54
  • Yes, well everything worked well and makes sense in the context it was written for. Then, this bottleneck threw a monkey wretch in the gears. I did, in fact, look at redesigning the bigger picture, but as I got into that effort it was clear that was going to be miserable. – BuvinJ Apr 22 '22 at 14:21
  • Thanks for the suggested solution. Cutting off one of those temp copies is great! Of course, if that reduces 50% of the work, that's not as much as my 99.9%. For this situation, I certainly wish the QVariants were some other type. Any type change, however, would be a major pita for me at this stage. I will keep in mind the `std::any` idea for the future. That's a really nice tip about how it would have provided the reference access needed here! – BuvinJ Apr 22 '22 at 14:33
  • Well... I tested this solution. While it looks really good on paper, this is slightly SLOWER than my original! – BuvinJ Apr 22 '22 at 22:13
  • So, despite all the harsh criticisms thrown at me for my answer (which I admitted from the jump was a big hack), I crushed it when you compare the numbers. Mine reduces the time to do a given job by more than 99%, while this one increases it ever so slightly - i.e. by about 1%. – BuvinJ Apr 22 '22 at 22:18
  • More than likely this answer is no faster, because of all the magical compiler optimizations that were being touted earlier... I.e. The compiler knew how to do it just as well written my first (admittedly terrible) way. But my hack of the `d` member is a whole other animal. – BuvinJ Apr 22 '22 at 22:24
-1

Got it!

I was on the right track, but had to perform a deep dive into the Qt source to figure out how to finish this off. I now have another macro to call upon, which takes me the rest of the way:

#define varPtrToListPtr( varPtr, ptrName ) \
    QVariantList *ptrName( varPtr->type() == QVariant::List ? \
        static_cast<QVariantList *>( static_cast<void *>( &(varPtr->d.data.c) ) ) \
        : nullptr );

So, here's the example from above with the rest of the story filled in:

const QString key( "mylist" );
QVariantMap map;
map[ key ] = QVariantList( {"a","b","c"} );           
getMapValuePtr( QString, QVariant, map, key, valuePtr )
if( !valuePtr ) return; // imagine this being inside a function...
varPtrToListPtr( valuePtr, valueListPtr )
if( valueListPtr ) *valueListPtr << QVariantList( {"x","y","z"} );

This achieves the same results as the straight forward one-liner I showed in the question, but now I'm avoiding all the overhead of creating and destroying temp copies of giant lists!

Admittedly, the initial search with the iterator might burn some time compared to using QMap::value( key ), but none of my maps here have many k/v pairs, so the overall gain of avoiding the list copies negates that.

Update:

  • I ran some tests using this method vs the original and observed it literally shave MORE than 99% of the time required off the same workload. Like doing a task in 50 ms rather than the painful 7.5 seconds the old way would take.

  • This was originally written on Windows, against MSVC. I discovered, however, that on other platforms/compilers this fails to build unless you make a 1 line tweak to the Qt base library:

    • Open the Qt header for class QVariant.
    • Search the file for the line containing: Private d;
    • Directly above that, add the line: public: // HACK!. That aligns other build contexts with the MSVC access to that class member.

Note: I fully acknowledge the "bad form", and the downsides, to this solution... BUT the payoff may be worth it for you. It was for my use case.

BuvinJ
  • 10,221
  • 5
  • 83
  • 96
  • I'd convert that `#define` into a function. This is C++, not C. Using a macro here offers no benefit. – hyde Apr 22 '22 at 13:45
  • 1
    @hyde even if it were C I'd write it as a function – Caleth Apr 22 '22 at 13:47
  • This is about shaving clock cycles. Functions add overhead and would slow down this optimization a bit. – BuvinJ Apr 22 '22 at 13:49
  • 1
    "Functions add overhead" no, they don't. Functions like that are ripe for inlining – Caleth Apr 22 '22 at 13:50
  • 1
    @BuvinJ If you are using `QVariant`, shaving off clock cycles must not be very high on your priorities. It's a convenience data type, which you pay with some overhead. Further, you should check if using QHash instead of QMap improves performance, it likely will if the map is large (`O(1)` vs `O(log n)` access time, it does really add up if `n` is large). – hyde Apr 22 '22 at 13:51
  • 1
    @BuvinJ A big performance issue seems to be `getMapValuePtr` does a linear search through all the items in the map. That is _really_ inefficient, compared to actually using a map or hash as they're meant to be used. Consider using `QMap::find`. – hyde Apr 22 '22 at 13:54
  • Inlining is out of your control. The compiler does that itself. The `inline` keyword doesn't even do anything any longer with many compilers. It might come out the same, but that's undefined. – BuvinJ Apr 22 '22 at 13:55
  • Also you've broken you windows install of Qt if it was letting you access the `d` pointer of anything – Caleth Apr 22 '22 at 13:55
  • @BuvinJ yes, that's a *good thing*. Let the compiler choose. Based on the code you've written, I'd bet it does a better job than you. – Caleth Apr 22 '22 at 13:56
  • I believe I previously acknowledged the performance hit of the key search. In my use case, it's not a factor though. Please note that I have proof this shaves 99.9% of the time in my use case. – BuvinJ Apr 22 '22 at 13:57
  • @Caleth Qt exposed `d` themselves in MSVC here. – BuvinJ Apr 22 '22 at 13:58
  • This is not an argument of what's "theoretically" best. I agree with the vast majority of these criticisms from that angle. This about my exact scenario. – BuvinJ Apr 22 '22 at 14:02
  • Oh, right above there there's the comment `// ### Qt6: FIXME: Remove the special Q_CC_MSVC handling, it was introduced to maintain BC for QTBUG-41810 .` – Caleth Apr 22 '22 at 14:03
  • Are you compiling with optimisations on? – Caleth Apr 22 '22 at 14:06
  • I revised my dreaded macro to use `QMap::find`, as suggested. That reads better. It didn't have ANY notable impact on the performance of the function though! As originally stated, all of the maps in this use case have a fairly small number of k/v pairs. It probably would prove to be faster if I were to recycle that macro into a scenario with a larger number of map entries though. – BuvinJ Apr 22 '22 at 23:07
  • Yes, I saw Qt's comment. I'm aware they would not want a `d` member exposed normally, and will end up shutting that down on MSVC as well for the class, in future versions from what I'm compiling on now. That wouldn't change anything on my end other than needing to apply the hack in one more place. The way I would get burnt would be if the members under `d` were to change (or how they were employed). That said, I rather doubt Qt is about to revise that QVariant code right now. If they do, there would again be one function to be addressed, as opposed to redesigning a pile of client classes. – BuvinJ Apr 22 '22 at 23:19