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