Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(301)

Side by Side Diff: pkg/compiler/lib/src/cps_ir/bounds_checker.dart

Issue 1353443002: dart2js cps: Add a pass for eliminating bounds checks. (Closed) Base URL: git@github.com:dart-lang/sdk.git@master
Patch Set: Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library dart2js.cps_ir.bounds_checker;
6
7 import 'cps_ir_nodes.dart';
8 import 'optimizers.dart' show Pass;
9 import 'octagon.dart';
10 import '../constants/values.dart';
11 import 'cps_fragment.dart';
12 import 'type_mask_system.dart';
13 import '../world.dart';
14 import '../elements/elements.dart';
15
16 /// Eliminates bounds checks when they can be proven safe.
17 ///
18 /// In general, this pass will try to eliminate any branch with arithmetic
19 /// in the condition, i.e. `x < y`, `x <= y`, `x == y` etc.
20 ///
21 /// The analysis uses an [Octagon] abstract domain. Unlike traditional octagon
22 /// analyzers, we do not use a closed matrix representation, but just maintain
23 /// a bucket of constraints. Constraints can therefore be added and removed
24 /// on-the-fly without significant overhead.
25 ///
26 /// We never copy the constraint system. While traversing the IR, the
27 /// constraint system is mutated to take into account the knowledge that is
28 /// valid for the current location. Constraints are added when entering a
29 /// branch, for instance, and removed again after the branch has been processed.
30 ///
31 /// Loops are analyzed in two passes. The first pass establishes monotonicity
32 /// of loop variables, which the second pass uses to compute upper/lower bounds.
33 /// The first pass also records whether any side effects occurred in the loop.
34 ///
35 /// The two-pass scheme is suboptimal compared to a least fixed-point
36 /// computation, but does not require repeated iteration. Repeated iteration
37 /// would be expensive, since we cannot perform a sparse analysis with our
38 /// mutable octagon representation.
39 class BoundsChecker extends RecursiveVisitor implements Pass {
40 String get passName => 'Bounds checker';
41
42 static const int MAX_UINT32 = (1 << 32) - 1;
43
44 /// All integers of this magnitude or less are representable as JS numbers.
45 static const int MAX_SAFE_INT = (1 << 53) - 1;
46
47 /// Marker to indicate that a continuation should get a unique effect number.
48 static const int NEW_EFFECT = -1;
49
50 final TypeMaskSystem types;
51 final World world;
52
53 /// Fields for the constraint system and its variables.
54 final Octagon octagon = new Octagon();
55 final Map<Primitive, SignedVariable> valueOf = {};
56 final Map<Primitive, Map<int, SignedVariable>> lengthOf = {};
57
58 /// Fields for the two-pass handling of loops.
59 final Set<Continuation> loopsWithSideEffects = new Set<Continuation>();
60 final Map<Parameter, Monotonicity> monotonicity = <Parameter, Monotonicity>{};
61 bool isStrongLoopPass;
62 bool foundLoop = false;
63
64 /// Fields for tracking side effects.
65 ///
66 /// The IR is divided into regions wherein the lengths of indexable objects
67 /// are known not to change. Regions are identified by their "effect number".
68 final Map<Continuation, int> effectNumberAt = <Continuation, int>{};
69 int currentEffectNumber = 0;
70 int effectNumberCounter = 0;
71
72 BoundsChecker(this.types, this.world);
73
74 void rewrite(FunctionDefinition node) {
75 isStrongLoopPass = false;
76 visit(node);
77 if (foundLoop) {
78 isStrongLoopPass = true;
79 effectNumberAt.clear();
80 visit(node);
81 }
82 }
83
84 /// ------------- VARIABLES -----------------
85
86 int makeNewEffect() => ++effectNumberCounter;
87
88 bool isInt(Primitive prim) {
89 return types.isDefinitelyInt(prim.type);
90 }
91
92 bool isUInt32(Primitive prim) {
93 return types.isDefinitelyUInt32(prim.type);
94 }
95
96 /// Get a constraint variable representing the numeric value of [number].
97 SignedVariable getValue(Primitive number) {
98 assert(isInt(number));
99 number = number.effectiveDefinition;
100 int min, max;
101 if (isUInt32(number)) {
sra1 2015/09/30 21:54:19 Is there a benefit to matching JSPositiveInt too?
asgerf 2015/10/01 09:49:34 Might as well match against it.
102 min = 0;
103 max = MAX_UINT32;
104 }
105 return valueOf.putIfAbsent(number, () => octagon.makeVariable(min, max));
106 }
107
108 /// Get a constraint variable representing the length of [indexableObject] at
109 /// program locations with the given [effectCounter].
110 SignedVariable getLength(Primitive indexableObject, int effectCounter) {
111 indexableObject = indexableObject.effectiveDefinition;
112 if (indexableObject.type != null &&
113 types.isDefinitelyFixedLengthIndexable(indexableObject.type)) {
114 // Always use the same effect counter if the length is immutable.
115 effectCounter = 0;
116 }
117 return lengthOf
118 .putIfAbsent(indexableObject, () => <int, SignedVariable>{})
119 .putIfAbsent(effectCounter, () => octagon.makeVariable(0, MAX_UINT32));
120 }
121
122 /// ------------- CONSTRAINT HELPERS -----------------
sra1 2015/09/30 21:54:19 Use '//'. This is a doc comment that applies to ap
asgerf 2015/10/01 09:49:33 Done.
123
124 // Puts the given constraint "in scope" by adding it to the octagon, and
125 // pushing a stack action that will remove it again.
126 void applyConstraint(SignedVariable v1, SignedVariable v2, int k) {
127 Constraint constraint = new Constraint(v1, v2, k);
128 octagon.pushConstraint(constraint);
129 pushAction(() => octagon.popConstraint(constraint));
130 }
131
132 /// Return true if we can prove that `v1 + v2 <= k`.
133 bool testConstraint(SignedVariable v1, SignedVariable v2, int k) {
134 // Add the negated constraint and check for solvability.
135 // !(v1 + v2 <= k) <==> -v1 - v2 <= -k-1
136 Constraint constraint = new Constraint(v1.negated, v2.negated, -k - 1);
137 octagon.pushConstraint(constraint);
138 bool answer = octagon.isUnsolvable;
139 octagon.popConstraint(constraint);
140 return answer;
141 }
142
143 void makeLessThanOrEqual(SignedVariable v1, SignedVariable v2) {
144 // v1 <= v2 <==> v1 - v2 <= 0
145 applyConstraint(v1, v2.negated, 0);
146 }
147
148 void makeLessThan(SignedVariable v1, SignedVariable v2) {
149 // v1 < v2 <==> v1 - v2 <= -1
150 applyConstraint(v1, v2.negated, -1);
151 }
152
153 void makeGreaterThanOrEqual(SignedVariable v1, SignedVariable v2) {
154 // v1 >= v2 <==> v2 - v1 <= 0
155 applyConstraint(v2, v1.negated, 0);
156 }
157
158 void makeGreaterThan(SignedVariable v1, SignedVariable v2) {
159 // v1 > v2 <==> v2 - v1 <= -1
160 applyConstraint(v2, v1.negated, -1);
161 }
162
163 void makeConstant(SignedVariable v1, int k) {
164 // We model this using the constraints:
165 // v1 + v1 <= 2k
166 // -v1 - v1 <= -2k
167 applyConstraint(v1, v1, 2 * k);
168 applyConstraint(v1.negated, v1.negated, -2 * k);
169 }
170
171 /// Make `v1 = v2 + k`.
172 void makeExactSum(SignedVariable v1, SignedVariable v2, int k) {
173 applyConstraint(v1, v2.negated, k);
174 applyConstraint(v1.negated, v2, -k);
175 }
176
177 /// Make `v1 = v2 [+] k` where [+] represents floating-point addition.
178 void makeFloatingPointSum(SignedVariable v1, SignedVariable v2, int k) {
179 if (isDefinitelyLessThanOrEqualToConstant(v2, MAX_SAFE_INT - k) &&
180 isDefinitelyGreaterThanOrEqualToConstant(v2, -MAX_SAFE_INT + k)) {
181 // The result is known to be in the 53-bit range, so no rounding occurs.
182 makeExactSum(v1, v2, k);
183 } else {
184 // A rounding error may occur, so the result may not be exactly v2 + k.
185 // We can still add monotonicity constraints:
186 // adding a positive number cannot return a lesser number
187 // adding a negative number cannot return a greater number
188 if (k >= 0) {
189 // v1 >= v2 <==> v2 - v1 <= 0 <==> -v1 + v2 <= 0
190 applyConstraint(v1.negated, v2, 0);
191 } else {
192 // v1 <= v2 <==> v1 - v2 <= 0
193 applyConstraint(v1, v2.negated, 0);
194 }
195 }
196 }
197
198 void makeEqual(SignedVariable v1, SignedVariable v2) {
199 // We model this using the constraints:
200 // v1 <= v2 <==> v1 - v2 <= 0
201 // v1 >= v2 <==> v2 - v1 <= 0
202 applyConstraint(v1, v2.negated, 0);
203 applyConstraint(v2, v1.negated, 0);
204 }
205
206 void makeNotEqual(SignedVariable v1, SignedVariable v2) {
207 // The octagon cannot represent non-equality, but we can sharpen a weak
208 // inequality to a sharp one. If v1 and v2 are already known to be equal,
209 // this will create a contradiction and eliminate a dead branch.
210 // This is necessary for eliminating concurrent modification checks.
211 if (isDefinitelyLessThanOrEqualTo(v1, v2)) {
212 makeLessThan(v1, v2);
213 } else if (isDefinitelyGreaterThanOrEqualTo(v1, v2)) {
214 makeGreaterThan(v1, v2);
215 }
216 }
217
218 /// Return true if we can prove that `v1 <= v2`.
219 bool isDefinitelyLessThanOrEqualTo(SignedVariable v1, SignedVariable v2) {
220 return testConstraint(v1, v2.negated, 0);
221 }
222
223 /// Return true if we can prove that `v1 >= v2`.
224 bool isDefinitelyGreaterThanOrEqualTo(SignedVariable v1, SignedVariable v2) {
225 return testConstraint(v2, v1.negated, 0);
226 }
227
228 bool isDefinitelyLessThanOrEqualToConstant(SignedVariable v1, int value) {
229 // v1 <= value <==> v1 + v1 <= 2 * value
230 return testConstraint(v1, v1, 2 * value);
231 }
232
233 bool isDefinitelyGreaterThanOrEqualToConstant(SignedVariable v1, int value) {
234 // v1 >= value <==> -v1 - v1 <= -2 * value
235 return testConstraint(v1.negated, v1.negated, -2 * value);
236 }
237
238 /// ------------- TAIL EXPRESSIONS -----------------
239
240 @override
241 void visitBranch(Branch node) {
242 Primitive condition = node.condition.definition;
243 Continuation trueCont = node.trueContinuation.definition;
244 Continuation falseCont = node.falseContinuation.definition;
245 effectNumberAt[trueCont] = currentEffectNumber;
246 effectNumberAt[falseCont] = currentEffectNumber;
247 pushAction(() {
248 // If the branching condition is known statically, either or both of the
249 // branch continuations will be replaced by Unreachable. Clean up the
250 // branch afterwards.
251 if (trueCont.body is Unreachable && falseCont.body is Unreachable) {
252 replaceExpression(node, new Unreachable());
253 } else if (trueCont.body is Unreachable) {
254 replaceExpression(
255 node, new InvokeContinuation(falseCont, <Parameter>[]));
256 } else if (falseCont.body is Unreachable) {
257 replaceExpression(
258 node, new InvokeContinuation(trueCont, <Parameter>[]));
259 }
260 });
261 void pushTrue(makeConstraint()) {
262 pushAction(() {
263 makeConstraint();
264 push(trueCont);
265 });
266 }
267 void pushFalse(makeConstraint()) {
268 pushAction(() {
269 makeConstraint();
270 push(falseCont);
271 });
272 }
273 if (condition is ApplyBuiltinOperator &&
274 condition.arguments.length == 2 &&
275 isInt(condition.arguments[0].definition) &&
276 isInt(condition.arguments[1].definition)) {
277 SignedVariable v1 = getValue(condition.arguments[0].definition);
278 SignedVariable v2 = getValue(condition.arguments[1].definition);
279 switch (condition.operator) {
280 case BuiltinOperator.NumLe:
281 pushTrue(() => makeLessThanOrEqual(v1, v2));
282 pushFalse(() => makeGreaterThan(v1, v2));
283 return;
284 case BuiltinOperator.NumLt:
285 pushTrue(() => makeLessThan(v1, v2));
286 pushFalse(() => makeGreaterThanOrEqual(v1, v2));
287 return;
288 case BuiltinOperator.NumGe:
289 pushTrue(() => makeGreaterThanOrEqual(v1, v2));
290 pushFalse(() => makeLessThan(v1, v2));
291 return;
292 case BuiltinOperator.NumGt:
293 pushTrue(() => makeGreaterThan(v1, v2));
294 pushFalse(() => makeLessThanOrEqual(v1, v2));
295 return;
296 case BuiltinOperator.StrictEq:
297 pushTrue(() => makeEqual(v1, v2));
298 pushFalse(() => makeNotEqual(v1, v2));
299 return;
300 case BuiltinOperator.StrictNeq:
301 pushTrue(() => makeNotEqual(v1, v2));
302 pushFalse(() => makeEqual(v1, v2));
303 return;
304 default:
305 }
306 }
307
308 push(trueCont);
309 push(falseCont);
310 }
311
312 @override
313 void visitConstant(Constant node) {
314 // TODO(asgerf): It might be faster to inline the constant in the
315 // constraints that reference it.
316 if (node.value.isInt) {
317 IntConstantValue constant = node.value;
318 makeConstant(getValue(node), constant.primitiveValue);
319 }
320 }
321
322 @override
323 void visitApplyBuiltinOperator(ApplyBuiltinOperator node) {
324 if (node.operator != BuiltinOperator.NumAdd &&
325 node.operator != BuiltinOperator.NumSubtract) {
326 return;
327 }
328 if (!isInt(node.arguments[0].definition) ||
329 !isInt(node.arguments[1].definition)) {
330 return;
331 }
332 // We have `v1 = v2 +/- v3`, but the octagon cannot represent constraints
333 // involving more than two variables. Check if one operand is a constant.
334 int getConstantArgument(int n) {
335 Primitive prim = node.arguments[n].definition;
336 if (prim is Constant && prim.value.isInt) {
337 IntConstantValue constant = prim.value;
338 return constant.primitiveValue;
339 }
340 return null;
341 }
342 int constant = getConstantArgument(0);
343 int operandIndex = 1;
344 if (constant == null) {
345 constant = getConstantArgument(1);
346 operandIndex = 0;
347 }
348 if (constant == null) {
349 // Neither argument was a constant.
350 // Classical octagon-based analyzers would compute upper and lower bounds
351 // for the two operands and add constraints for the result based on
352 // those. For performance reasons we omit that.
353 // TODO(asgerf): It seems expensive, but we should evaluate it.
354 return;
355 }
356 SignedVariable v1 = getValue(node);
357 SignedVariable v2 = getValue(node.arguments[operandIndex].definition);
358
359 if (node.operator == BuiltinOperator.NumAdd) {
360 // v1 = v2 + const
361 makeFloatingPointSum(v1, v2, constant);
362 } else if (operandIndex == 0) {
363 // v1 = v2 - const
364 makeFloatingPointSum(v1, v2, -constant);
365 } else {
366 // v1 = const - v2 <==> v1 = (-v2) + const
367 makeFloatingPointSum(v1, v2.negated, constant);
368 }
369 }
370
371 @override
372 void visitGetLength(GetLength node) {
373 valueOf[node] = getLength(node.object.definition, currentEffectNumber);
374 }
375
376 void analyzeLoopEntry(InvokeContinuation node) {
377 foundLoop = true;
378 Continuation cont = node.continuation.definition;
379 if (isStrongLoopPass) {
380 for (int i = 0; i < node.arguments.length; ++i) {
381 Parameter param = cont.parameters[i];
382 if (!isInt(param)) continue;
383 Primitive initialValue = node.arguments[i].definition;
384 SignedVariable initialVariable = getValue(initialValue);
385 Monotonicity mono = monotonicity[param];
386 if (mono == null) {
387 // Value never changes. This is extremely uncommon.
388 initialValue.substituteFor(param);
389 } else if (mono == Monotonicity.Increasing) {
390 makeGreaterThanOrEqual(getValue(param), initialVariable);
391 } else if (mono == Monotonicity.Decreasing) {
392 makeLessThanOrEqual(getValue(param), initialVariable);
393 }
394 }
395 if (loopsWithSideEffects.contains(cont)) {
396 currentEffectNumber = makeNewEffect();
397 }
398 } else {
399 // During the weak pass, conservatively make a new effect number in the
400 // loop body. This may be strengthened during the strong pass.
401 currentEffectNumber = effectNumberAt[cont] = makeNewEffect();
402 }
403 push(cont);
404 }
405
406 void analyzeLoopContinue(InvokeContinuation node) {
407 Continuation cont = node.continuation.definition;
408
409 // During the strong loop phase, there is no need to compute monotonicity,
410 // and we already put bounds on the loop variables when we went into the
411 // loop.
412 if (isStrongLoopPass) return;
413
414 // For each loop parameter, try to prove that the new value is definitely
415 // less/greater than its old value. When we fail to prove this, update the
416 // monotonicity flag accordingly.
417 for (int i = 0; i < node.arguments.length; ++i) {
418 Parameter param = cont.parameters[i];
419 if (!isInt(param)) continue;
420 SignedVariable arg = getValue(node.arguments[i].definition);
421 SignedVariable paramVar = getValue(param);
422 if (!isDefinitelyLessThanOrEqualTo(arg, paramVar)) {
423 // We couldn't prove that the value does not increase, so assume
424 // henceforth that it might be increasing.
425 markMonotonicity(cont.parameters[i], Monotonicity.Increasing);
426 }
427 if (!isDefinitelyGreaterThanOrEqualTo(arg, paramVar)) {
428 // We couldn't prove that the value does not decrease, so assume
429 // henceforth that it might be decrease.
430 markMonotonicity(cont.parameters[i], Monotonicity.Decreasing);
431 }
432 }
433
434 // If a side effect has occurred between the entry and continue, mark
435 // the loop as having side effects.
436 if (currentEffectNumber != effectNumberAt[cont]) {
437 loopsWithSideEffects.add(cont);
438 }
439 }
440
441 void markMonotonicity(Parameter param, Monotonicity mono) {
442 Monotonicity current = monotonicity[param];
443 if (current == null) {
444 monotonicity[param] = mono;
445 } else if (current != mono) {
446 monotonicity[param] = Monotonicity.NotMonotone;
447 }
448 }
449
450 @override
451 void visitInvokeContinuation(InvokeContinuation node) {
452 Continuation cont = node.continuation.definition;
453 if (node.isRecursive) {
454 analyzeLoopContinue(node);
455 } else if (cont.isRecursive) {
456 analyzeLoopEntry(node);
457 } else {
458 int effect = effectNumberAt[cont];
459 if (effect == null) {
460 effectNumberAt[cont] = currentEffectNumber;
461 } else if (effect != currentEffectNumber && effect != NEW_EFFECT) {
462 effectNumberAt[cont] = NEW_EFFECT;
463 }
464 // TODO(asgerf): Compute join for parameters to increase precision?
465 }
466 }
467
468 /// ---------------- CALL EXPRESSIONS --------------------
469
470 @override
471 void visitInvokeMethod(InvokeMethod node) {
472 // TODO(asgerf): What we really need is a "changes length" side effect flag.
sra1 2015/09/30 21:54:20 More than that - with aliases so a.add(b[i]) does
asgerf 2015/10/01 09:49:33 Absolutely. I just wanted to clarify why we are us
473 if (world
474 .getSideEffectsOfSelector(node.selector, node.mask)
475 .changesIndex()) {
476 currentEffectNumber = makeNewEffect();
477 }
478 push(node.continuation.definition);
479 }
480
481 @override
482 void visitInvokeStatic(InvokeStatic node) {
483 if (world.getSideEffectsOfElement(node.target).changesIndex()) {
484 currentEffectNumber = makeNewEffect();
485 }
486 push(node.continuation.definition);
487 }
488
489 @override
490 void visitInvokeMethodDirectly(InvokeMethodDirectly node) {
491 FunctionElement target = node.target;
492 if (target is ConstructorBodyElement) {
493 ConstructorBodyElement body = target;
494 target = body.constructor;
495 }
496 if (world.getSideEffectsOfElement(target).changesIndex()) {
497 currentEffectNumber = makeNewEffect();
498 }
499 push(node.continuation.definition);
500 }
501
502 @override
503 void visitInvokeConstructor(InvokeConstructor node) {
504 if (world.getSideEffectsOfElement(node.target).changesIndex()) {
505 currentEffectNumber = makeNewEffect();
506 }
507 push(node.continuation.definition);
508 }
509
510 @override
511 void visitTypeCast(TypeCast node) {
512 push(node.continuation.definition);
513 }
514
515 @override
516 void visitGetLazyStatic(GetLazyStatic node) {
517 // TODO(asgerf): How do we get the side effects of a lazy field initializer?
518 currentEffectNumber = makeNewEffect();
519 push(node.continuation.definition);
520 }
521
522 @override
523 void visitForeignCode(ForeignCode node) {
524 if (node.nativeBehavior.sideEffects.changesIndex()) {
525 currentEffectNumber = makeNewEffect();
526 }
527 push(node.continuation.definition);
528 }
529
530 @override
531 void visitAwait(Await node) {
532 currentEffectNumber = makeNewEffect();
533 push(node.continuation.definition);
534 }
535
536 /// ---------------- PRIMITIVES --------------------
537
538 @override
539 void visitApplyBuiltinMethod(ApplyBuiltinMethod node) {
540 Primitive receiver = node.receiver.definition;
541 int effectBefore = currentEffectNumber;
542 currentEffectNumber = makeNewEffect();
543 int effectAfter = currentEffectNumber;
544 SignedVariable lengthBefore = getLength(receiver, effectBefore);
545 SignedVariable lengthAfter = getLength(receiver, effectAfter);
546 switch (node.method) {
547 case BuiltinMethod.Push:
548 // after = before + count
549 int count = node.arguments.length;
550 makeExactSum(lengthAfter, lengthBefore, count);
551 break;
552
553 case BuiltinMethod.Pop:
554 // after = before - 1
555 makeExactSum(lengthAfter, lengthBefore, -1);
556 break;
557 }
558 }
559
560 @override
561 void visitLiteralList(LiteralList node) {
562 makeConstant(getLength(node, currentEffectNumber), node.values.length);
563 }
564
565 /// ---------------- INTERIOR EXPRESSIONS --------------------
566
567 @override
568 Expression traverseContinuation(Continuation cont) {
569 if (octagon.isUnsolvable) {
570 RemovalVisitor.remove(cont.body);
571 cont.body = new Unreachable();
572 } else {
573 int effect = effectNumberAt[cont];
574 if (effect != null) {
575 currentEffectNumber = effect == NEW_EFFECT ? makeNewEffect() : effect;
576 }
577 }
578 return cont.body;
579 }
580
581 @override
582 Expression traverseLetCont(LetCont node) {
583 // Join continuations should be pushed at declaration-site, so all their
584 // call sites are seen before they are analyzed.
585 // Other continuations are pushed at the use site.
586 for (Continuation cont in node.continuations) {
587 if (cont.hasAtLeastOneUse &&
588 !cont.isRecursive &&
589 cont.firstRef.parent is InvokeContinuation) {
590 push(cont);
591 }
592 }
593 return node.body;
594 }
595 }
596
597 /// Lattice representing the known monotonicity of a loop variable.
598 ///
599 /// The lattice bottom is represented by `null` and represents the case where
600 /// the loop variable never changes value during the loop.
sra1 2015/09/30 21:54:19 These are weak monotonicity, not strict, right?
asgerf 2015/10/01 09:49:33 Yes. Clarified in the comment.
601 enum Monotonicity { NotMonotone, Increasing, Decreasing, }
OLDNEW
« no previous file with comments | « no previous file | pkg/compiler/lib/src/cps_ir/cps_fragment.dart » ('j') | pkg/compiler/lib/src/cps_ir/type_mask_system.dart » ('J')

Powered by Google App Engine
This is Rietveld 408576698