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