Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmljsoptimizations.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3// Qt-Security score:significant
4
8
9#include <QtCore/qhash.h>
10#include <QtCore/qset.h>
11
13
14using namespace Qt::Literals::StringLiterals;
15
16QQmlJSCompilePass::BlocksAndAnnotations QQmlJSOptimizations::run(const Function *function)
17{
18 m_function = function;
19
20 populateBasicBlocks();
21 populateReaderLocationsTrackedTypes();
22 populateReaderLocationsReadersAndConversions();
23 removeDeadStoresUntilStable();
24 adjustTypes();
25
26 return { std::move(m_basicBlocks), std::move(m_annotations) };
27}
28
29template<typename ContainerA, typename ContainerB>
30static bool containsAny(const ContainerA &container, const ContainerB &elements)
31{
32 for (const auto &element : elements) {
33 if (container.contains(element))
34 return true;
35 }
36 return false;
37}
38
39template<class Key, class T, class Compare = std::less<Key>,
40 class KeyContainer = QList<Key>, class MappedContainer = QList<T>>
42{
43public:
45
46 void appendOrdered(const typename OriginalFlatMap::iterator &i)
47 {
48 keys.append(i.key());
49 values.append(i.value());
50 }
51
53 {
54 OriginalFlatMap result(Qt::OrderedUniqueRange, std::move(keys), std::move(values));
55 keys.clear();
56 values.clear();
57 return result;
58 }
59
60private:
61 typename OriginalFlatMap::key_container_type keys;
62 typename OriginalFlatMap::mapped_container_type values;
63};
64
65void QQmlJSOptimizations::populateReaderLocationsTrackedTypes()
66{
67 for (const auto &[offset, annotation] : m_annotations) {
68 const int writtenRegister = annotation.changedRegisterIndex;
69
70 // Instructions that don't write can't be dead stores, no need to populate reader locations
71 if (writtenRegister == InvalidRegister)
72 continue;
73
74 RegisterAccess &access = m_readerLocations[offset];
75 access.trackedRegister = writtenRegister;
76 if (annotation.changedRegister.isConversion()) {
77 // If it's a conversion, we have to check for all readers of the conversion origins.
78 // This happens at jump targets where different types are merged. A StoreReg or similar
79 // instruction must be optimized out if none of the types it can hold is read anymore.
80 const auto &origins = annotation.changedRegister.conversionOrigins();
81 for (QQmlJSRegisterContent origin : origins)
82 access.trackedTypes.append(origin);
83 } else {
84 access.trackedTypes.append(annotation.changedRegister);
85 Q_ASSERT(!access.trackedTypes.last().isNull());
86 }
87 }
88}
89
90// Every write that currently reaches this program point for this register, and the
91// type-conversion instructions crossed since each one specifically. Almost always a
92// single entry; more than one only at a genuine merge of differing reaching writes.
93using RegisterState = QHash<int, QQmlJSOptimizations::Conversions>;
94
96{
97 QHash<QQmlJSRegisterContent, QSet<int>> availableContent; // content -> writers reaching here
98 QHash<int, RegisterState> registers; // register index -> reaching state
99};
100
101static bool mergeBlockState(BlockState &to, const BlockState &from)
102{
103 bool changed = false;
104
105 for (auto contentIt = from.availableContent.constBegin(),
106 contentEnd = from.availableContent.constEnd(); contentIt != contentEnd; ++contentIt) {
107 QSet<int> &target = to.availableContent[contentIt.key()];
108 const qsizetype before = target.size();
109 target.unite(contentIt.value());
110 if (target.size() > before)
111 changed = true;
112 }
113
114 for (const auto &[index, registerState] : from.registers.asKeyValueRange()) {
115 RegisterState &target = to.registers[index];
116 for (const auto &[writeOffset, conversions] : registerState.asKeyValueRange()) {
117 const auto existing = target.find(writeOffset);
118 if (existing == target.end()) {
119 target.insert(writeOffset, conversions);
120 changed = true;
121 continue;
122 }
123 const qsizetype before = existing.value().size();
124 existing.value().unite(conversions);
125 if (existing.value().size() > before)
126 changed = true;
127 }
128 }
129
130 return changed;
131}
132
133void QQmlJSOptimizations::populateReaderLocationsReadersAndConversions()
134{
135 QHash<int, BlockState> entryState; // block start -> state at block entry
136 std::vector<int> pending;
137 pending.reserve(m_basicBlocks.size());
138 for (const auto &[blockStart, block] : m_basicBlocks)
139 pending.push_back(blockStart);
140
141 while (!pending.empty()) {
142 const int blockStart = pending.back();
143 pending.pop_back();
144
145 const auto blockIt = m_basicBlocks.find(blockStart);
146 auto nextBlockIt = blockIt;
147 ++nextBlockIt;
148
149 BlockState state = entryState.value(blockStart);
150
151 auto instrIt = m_annotations.find(blockStart);
152 const auto blockEnd = (nextBlockIt == m_basicBlocks.end())
153 ? m_annotations.end()
154 : m_annotations.find(nextBlockIt->first);
155
156 for (; instrIt != blockEnd; ++instrIt) {
157 const int key = instrIt.key();
158 if (!instrIt->second.isRename) {
159 for (const auto &read : instrIt->second.readRegisters.values()) {
160 const QQmlJSRegisterContent &content = read.content;
161 const auto recordRead = [&](const QQmlJSRegisterContent &r) {
162 const auto found = state.availableContent.constFind(r);
163 if (found != state.availableContent.constEnd()) {
164 for (int writerKey : found.value())
165 m_readerLocations[writerKey].typeReaders[key] = content;
166 }
167 };
168
169 if (content.isConversion()) {
170 Q_ASSERT(content.conversionResultType());
171 for (QQmlJSRegisterContent origin : content.conversionOrigins())
172 recordRead(origin);
173 } else {
174 recordRead(content);
175 }
176 }
177 }
178
179 for (const int convRegister : instrIt->second.typeConversions.keys()) {
180 const auto tracked = state.registers.find(convRegister);
181 if (tracked != state.registers.end()) {
182 for (auto writerIt = tracked.value().begin(), writerEnd = tracked.value().end();
183 writerIt != writerEnd; ++writerIt) {
184 writerIt.value().insert(key);
185 }
186 }
187 }
188
189 for (const int readRegister : instrIt->second.readRegisters.keys()) {
190 const auto tracked = state.registers.constFind(readRegister);
191 if (tracked != state.registers.constEnd()) {
192 for (const auto &[writerKey, value] : tracked.value().asKeyValueRange())
193 m_readerLocations[writerKey].registerReadersAndConversions[key] = value;
194 }
195 }
196
197 // Record this instruction's write, once done reading above.
198 if (instrIt->second.changedRegisterIndex != InvalidRegister) {
199 const auto access = m_readerLocations.constFind(key);
200 if (access != m_readerLocations.constEnd()) {
201 for (const QQmlJSRegisterContent &tracked : std::as_const(access->trackedTypes))
202 state.availableContent[tracked].insert(key);
203 }
204
205 // A write unconditionally shadows whatever reached this register before it
206 RegisterState fresh;
207 fresh.insert(key, Conversions{});
208 state.registers[instrIt->second.changedRegisterIndex] = std::move(fresh);
209 }
210 }
211
212 auto scheduleSuccessor = [&](int successorStart) {
213 if (mergeBlockState(entryState[successorStart], state))
214 pending.push_back(successorStart);
215 };
216
217 if (!blockIt->second.jumpIsUnconditional && nextBlockIt != m_basicBlocks.end())
218 scheduleSuccessor(nextBlockIt->first);
219
220 const int jumpTarget = blockIt->second.jumpTarget;
221 if (jumpTarget != -1)
222 scheduleSuccessor(jumpTarget);
223 }
224}
225
226bool QQmlJSOptimizations::eraseDeadStore(const InstructionAnnotations::iterator &it,
227 bool &erasedReaders)
228{
229 auto reader = m_readerLocations.find(it.key());
230 if (reader != m_readerLocations.end()
231 && (reader->typeReaders.isEmpty() || reader->registerReadersAndConversions.isEmpty())) {
232
233 if (it->second.isRename) {
234 // If it's a rename, it doesn't "own" its output type. The type may
235 // still be read elsewhere, even if this register isn't. However, we're
236 // not interested in the variant or any other details of the register.
237 // Therefore just delete it.
238 it->second.changedRegisterIndex = InvalidRegister;
239 it->second.changedRegister = QQmlJSRegisterContent();
240 } else {
241 // We can't do this with certain QObjects because they still need tracking as
242 // implicitly destructible by the garbage collector. We may be calling a factory
243 // function and then forgetting the object after all.
244 //
245 // However, objects we need to track that way can only be produced through external
246 // side effects (i.e. function calls).
247
248 const QQmlJSScope::ConstPtr contained = it->second.changedRegister.containedType();
249 if (!it->second.hasExternalSideEffects
250 || (!contained->isReferenceType()
251 && !m_typeResolver->canHold(contained, m_typeResolver->qObjectType()))) {
252 // void the output, rather than deleting it. We still need its variant.
253 const bool adjusted = m_typeResolver->adjustTrackedType(
254 it->second.changedRegister, m_typeResolver->voidType());
255 Q_ASSERT(adjusted); // Can always convert to void
256 }
257 }
258 m_readerLocations.erase(reader);
259
260 // If it's not a label and has no side effects, we can drop the instruction.
261 if (!it->second.hasInternalSideEffects) {
262 if (!it->second.readRegisters.isEmpty()) {
263 it->second.readRegisters.clear();
264 erasedReaders = true;
265 }
266 if (m_basicBlocks.find(it.key()) == m_basicBlocks.end())
267 return true;
268 }
269 }
270 return false;
271}
272
273void QQmlJSOptimizations::removeDeadStoresUntilStable()
274{
275 using NewInstructionAnnotations = NewFlatMap<int, InstructionAnnotation>;
276 NewInstructionAnnotations newAnnotations;
277
278 bool erasedReaders = true;
279 while (erasedReaders) {
280 erasedReaders = false;
281
282 for (auto it = m_annotations.begin(), end = m_annotations.end(); it != end; ++it) {
283 InstructionAnnotation &instruction = it->second;
284
285 // Don't touch the function prolog instructions
286 if (instruction.changedRegisterIndex < InvalidRegister) {
287 newAnnotations.appendOrdered(it);
288 continue;
289 }
290
291 removeReadsFromErasedInstructions(it);
292
293 if (!eraseDeadStore(it, erasedReaders))
294 newAnnotations.appendOrdered(it);
295 }
296
297 m_annotations = newAnnotations.take();
298 }
299}
300
301void QQmlJSOptimizations::removeReadsFromErasedInstructions(
302 const QFlatMap<int, InstructionAnnotation>::const_iterator &it)
303{
304 auto readers = m_readerLocations.find(it.key());
305 if (readers == m_readerLocations.end())
306 return;
307
308 for (auto typeIt = readers->typeReaders.begin(); typeIt != readers->typeReaders.end();) {
309 if (m_annotations.contains(typeIt.key()))
310 ++typeIt;
311 else
312 typeIt = readers->typeReaders.erase(typeIt);
313 }
314
315 for (auto registerIt = readers->registerReadersAndConversions.begin();
316 registerIt != readers->registerReadersAndConversions.end();) {
317 if (m_annotations.contains(registerIt.key()))
318 ++registerIt;
319 else
320 registerIt = readers->registerReadersAndConversions.erase(registerIt);
321 }
322}
323
324bool QQmlJSOptimizations::canMove(int instructionOffset,
325 const QQmlJSOptimizations::RegisterAccess &access) const
326{
327 if (access.typeReaders.size() != 1)
328 return false;
329 return QQmlJSBasicBlocks::constBasicBlockForInstruction(m_basicBlocks, instructionOffset)
330 == QQmlJSBasicBlocks::constBasicBlockForInstruction(m_basicBlocks, access.typeReaders.begin().key());
331}
332
333QList<QQmlJSCompilePass::ObjectOrArrayDefinition>
334QQmlJSBasicBlocks::objectAndArrayDefinitions() const
335{
336 return m_objectAndArrayDefinitions;
337}
338
340 QQmlJSRegisterContent origin, const QQmlJSScope::ConstPtr &conversion) {
341 return QLatin1String("Cannot convert from ")
342 + origin.containedType()->internalName() + QLatin1String(" to ")
343 + conversion->internalName();
344}
345
347 QQmlJSRegisterContent origin, QQmlJSRegisterContent conversion) {
348 return adjustErrorMessage(origin, conversion.containedType());
349}
350
352 QQmlJSRegisterContent origin, const QList<QQmlJSRegisterContent> &conversions) {
353 if (conversions.size() == 1)
354 return adjustErrorMessage(origin, conversions[0]);
355
356 QString types;
357 for (QQmlJSRegisterContent type : conversions) {
358 if (!types.isEmpty())
359 types += QLatin1String(", ");
360 types += type.containedType()->internalName();
361 }
362 return QLatin1String("Cannot convert from ")
363 + origin.containedType()->internalName() + QLatin1String(" to union of ") + types;
364}
365
366void QQmlJSOptimizations::adjustTypes()
367{
368 using NewVirtualRegisters = NewFlatMap<int, VirtualRegister>;
369
370 QHash<int, QList<int>> liveConversions;
371 QHash<int, QList<int>> movableReads;
372
373 const auto handleRegisterReadersAndConversions
374 = [&](QHash<int, RegisterAccess>::const_iterator it) {
375 for (auto conversions = it->registerReadersAndConversions.constBegin(),
376 end = it->registerReadersAndConversions.constEnd(); conversions != end;
377 ++conversions) {
378 if (conversions->isEmpty() && canMove(it.key(), it.value()))
379 movableReads[conversions.key()].append(it->trackedRegister);
380 for (int conversion : *conversions)
381 liveConversions[conversion].append(it->trackedRegister);
382 }
383 };
384
385 // Handle the array definitions first.
386 // Changing the array type changes the expected element types.
387 auto adjustArray = [&](int instructionOffset, int mode) {
388 auto it = m_readerLocations.constFind(instructionOffset);
389 if (it == m_readerLocations.cend())
390 return;
391
392 const InstructionAnnotation &annotation = m_annotations[instructionOffset];
393 if (annotation.readRegisters.isEmpty())
394 return;
395
396 Q_ASSERT(it->trackedTypes.size() == 1);
397 Q_ASSERT(it->trackedTypes[0] == annotation.changedRegister);
398
399 if (it->trackedTypes[0].containedType()->accessSemantics()
400 != QQmlJSScope::AccessSemantics::Sequence) {
401 return; // Constructed something else.
402 }
403
404 if (!m_typeResolver->adjustTrackedType(it->trackedTypes[0], it->typeReaders.values()))
405 addError(adjustErrorMessage(it->trackedTypes[0], it->typeReaders.values()));
406
407 // Now we don't adjust the type we store, but rather the type we expect to read. We
408 // can do this because we've tracked the read type when we defined the array in
409 // QQmlJSTypePropagator.
410 if (const QQmlJSScope::ConstPtr elementType
411 = it->trackedTypes[0].containedType()->elementType()) {
412 const auto adjust = [&](const auto it) {
413 const QQmlJSRegisterContent content = it.value().content;
414 const QQmlJSScope::ConstPtr contained = content.containedType();
415 if (!m_typeResolver->adjustTrackedType(content, elementType)) {
416 addError(adjustErrorMessage(content, elementType));
417 return false;
418 }
419 return true;
420 };
421
422 const auto &readRegisters = annotation.readRegisters;
423 if (mode == ObjectOrArrayDefinition::ArrayConstruct1ArgId) {
424 Q_ASSERT(readRegisters.size() == 1);
425 const auto it = readRegisters.cbegin();
426 if (it.value().content.containedType() != m_typeResolver->realType())
427 adjust(it);
428 } else {
429 for (auto it = readRegisters.cbegin(); it != readRegisters.cend(); ++it) {
430 if (!adjust(it))
431 break;
432 }
433 }
434 }
435
436 handleRegisterReadersAndConversions(it);
437 m_readerLocations.erase(it);
438 };
439
440 // Handle the object definitions.
441 // Changing the object type changes the expected property types.
442 const auto adjustObject = [&](const ObjectOrArrayDefinition &object) {
443 auto it = m_readerLocations.find(object.instructionOffset);
444 if (it == m_readerLocations.end())
445 return;
446
447 const InstructionAnnotation &annotation = m_annotations[object.instructionOffset];
448
449 Q_ASSERT(it->trackedTypes.size() == 1);
450 const QQmlJSRegisterContent resultType = it->trackedTypes[0];
451
452 Q_ASSERT(resultType == annotation.changedRegister);
453 Q_ASSERT(!annotation.readRegisters.isEmpty());
454
455 if (!m_typeResolver->adjustTrackedType(resultType, it->typeReaders.values()))
456 addError(adjustErrorMessage(resultType, it->typeReaders.values()));
457
458 m_readerLocations.erase(it);
459
460 if (resultType.contains(m_typeResolver->varType())
461 || resultType.contains(m_typeResolver->variantMapType())
462 || resultType.contains(m_typeResolver->jsValueType())) {
463 // It's all variant anyway
464 return;
465 }
466
467 const int classSize = m_jsUnitGenerator->jsClassSize(object.internalClassId);
468 Q_ASSERT(object.argc >= classSize);
469
470 for (int i = 0; i < classSize; ++i) {
471 // Now we don't adjust the type we store, but rather the types we expect to read. We
472 // can do this because we've tracked the read types when we defined the object in
473 // QQmlJSTypePropagator.
474
475 const QString propName = m_jsUnitGenerator->jsClassMember(object.internalClassId, i);
476 const QQmlJSMetaProperty property = resultType.containedType()->property(propName);
477 if (!property.isValid()) {
478 addError(resultType.containedType()->internalName()
479 + QLatin1String(" has no property called ") + propName);
480 continue;
481 }
482 const QQmlJSScope::ConstPtr propType = property.type();
483 if (propType.isNull()) {
484 addError(QLatin1String("Cannot resolve type of property ") + propName);
485 continue;
486 }
487 const QQmlJSRegisterContent content = annotation.readRegisters[object.argv + i].content;
488 if (!m_typeResolver->adjustTrackedType(content, propType))
489 addError(adjustErrorMessage(content, propType));
490 }
491
492 // The others cannot be adjusted. We don't know their names, yet.
493 // But we might still be able to use the variants.
494 };
495
496 // Iterate in reverse so that we can have nested lists and objects and the types are propagated
497 // from the outer lists/objects to the inner ones.
498 for (auto it = m_objectAndArrayDefinitions.crbegin(), end = m_objectAndArrayDefinitions.crend();
499 it != end; ++it) {
500 switch (it->internalClassId) {
501 case ObjectOrArrayDefinition::ArrayClassId:
502 case ObjectOrArrayDefinition::ArrayConstruct1ArgId:
503 adjustArray(it->instructionOffset, it->internalClassId);
504 break;
505 default:
506 adjustObject(*it);
507 break;
508 }
509 }
510
511 for (auto it = m_readerLocations.cbegin(), end = m_readerLocations.cend(); it != end; ++it) {
512 handleRegisterReadersAndConversions(it);
513
514 // There is always one first occurrence of any tracked type. Conversions don't change
515 // the type.
516 if (it->trackedTypes.size() != 1)
517 continue;
518
519 // Don't adjust renamed values. We only adjust the originals.
520 const int writeLocation = it.key();
521 if (writeLocation >= 0 && m_annotations[writeLocation].isRename)
522 continue;
523
524 if (!m_typeResolver->adjustTrackedType(it->trackedTypes[0], it->typeReaders.values()))
525 addError(adjustErrorMessage(it->trackedTypes[0], it->typeReaders.values()));
526 }
527
528
529 NewVirtualRegisters newRegisters;
530 for (auto i = m_annotations.begin(), iEnd = m_annotations.end(); i != iEnd; ++i) {
531 for (auto conversion = i->second.typeConversions.begin(),
532 conversionEnd = i->second.typeConversions.end(); conversion != conversionEnd;
533 ++conversion) {
534 if (!liveConversions[i.key()].contains(conversion.key()))
535 continue;
536
537 QQmlJSScope::ConstPtr newResult;
538 const auto content = conversion->second.content;
539 if (content.isConversion() && !content.original().isValid()) {
540 const auto &conversionOrigins = content.conversionOrigins();
541 for (const auto &origin : conversionOrigins)
542 newResult = m_typeResolver->merge(newResult, origin.containedType());
543 if (!m_typeResolver->adjustTrackedType(content, newResult))
544 addError(adjustErrorMessage(content, newResult));
545 }
546 newRegisters.appendOrdered(conversion);
547 }
548 i->second.typeConversions = newRegisters.take();
549
550 for (int movable : std::as_const(movableReads[i.key()]))
551 i->second.readRegisters[movable].canMove = true;
552 }
553}
554
555void QQmlJSOptimizations::populateBasicBlocks()
556{
557 for (auto blockNext = m_basicBlocks.begin(), blockEnd = m_basicBlocks.end();
558 blockNext != blockEnd;) {
559
560 const auto blockIt = blockNext++;
561 BasicBlock &block = blockIt->second;
562 QList<QQmlJSScope::ConstPtr> writtenTypes;
563 QList<int> writtenRegisters;
564
565 const auto instrEnd = (blockNext == blockEnd) ? m_annotations.end()
566 : m_annotations.find(blockNext->first);
567 for (auto instrIt = m_annotations.find(blockIt->first); instrIt != instrEnd; ++instrIt) {
568 const InstructionAnnotation &instruction = instrIt->second;
569 for (auto it = instruction.readRegisters.begin(), end = instruction.readRegisters.end();
570 it != end; ++it) {
571 if (!writtenRegisters.contains(it->first))
572 block.readRegisters.append(it->first);
573 }
574
575 // If it's just a renaming, the type has existed in a different register before.
576 if (instruction.changedRegisterIndex != InvalidRegister) {
577 if (!instruction.isRename)
578 writtenTypes.append(instruction.changedRegister.containedType());
579 writtenRegisters.append(instruction.changedRegisterIndex);
580 }
581 }
582
583 QQmlJSUtils::deduplicate(block.readRegisters);
584 }
585}
586
587
588QT_END_NAMESPACE
void appendOrdered(const typename OriginalFlatMap::iterator &i)
OriginalFlatMap take()
Combined button and popup list for selecting options.
static bool mergeBlockState(BlockState &to, const BlockState &from)
static QString adjustErrorMessage(QQmlJSRegisterContent origin, const QQmlJSScope::ConstPtr &conversion)
static bool containsAny(const ContainerA &container, const ContainerB &elements)
static QString adjustErrorMessage(QQmlJSRegisterContent origin, QQmlJSRegisterContent conversion)
QHash< int, RegisterState > registers