-
Notifications
You must be signed in to change notification settings - Fork 515
/
CoreOptimizeGeneratedCode.cs
1093 lines (968 loc) · 38.4 KB
/
CoreOptimizeGeneratedCode.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2012-2013, 2016 Xamarin Inc. All rights reserved.
using System;
using ObjCRuntime;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Linker;
using Mono.Tuner;
using Xamarin.Bundler;
using MonoTouch.Tuner;
#if MONOMAC
using MonoMac.Tuner;
#endif
#if NET
using Mono.Linker.Steps;
#endif
namespace Xamarin.Linker {
public class OptimizeGeneratedCodeSubStep : ExceptionalSubStep {
// If the type currently being processed is a direct binding or not.
// A null value means it's not a constant value, and can't be inlined.
bool? isdirectbinding_constant;
protected override string Name { get; } = "Binding Optimizer";
protected override int ErrorCode { get; } = 2020;
protected bool HasOptimizableCode { get; private set; }
protected bool IsExtensionType { get; private set; }
public bool IsDualBuild {
get { return LinkContext.App.IsDualBuild; }
}
public bool Device {
get { return LinkContext.App.IsDeviceBuild; }
}
public int Arch {
get { return LinkContext.Target.Is64Build ? 8 : 4; }
}
protected Optimizations Optimizations {
get {
return LinkContext.App.Optimizations;
}
}
// This is per assembly, so we set it in 'void Process (AssemblyDefinition)'
bool InlineIntPtrSize { get; set; }
bool? is_arm64_calling_convention;
public override void Initialize (LinkContext context)
{
base.Initialize (context);
if (Optimizations.InlineIsARM64CallingConvention == true) {
var target = LinkContext.Target;
if (target.Abis.Count == 1) {
// We can usually inline Runtime.InlineIsARM64CallingConvention if the generated code will execute on a single architecture
switch ((target.Abis [0] & Abi.ArchMask)) {
case Abi.i386:
case Abi.ARMv7:
case Abi.ARMv7s:
case Abi.x86_64:
is_arm64_calling_convention = false;
break;
case Abi.ARM64:
case Abi.ARM64e:
case Abi.ARM64_32:
is_arm64_calling_convention = true;
break;
case Abi.ARMv7k:
// ARMv7k binaries can run on ARM64_32, so this can't be inlined :/
break;
default:
LinkContext.Exceptions.Add (ErrorHelper.CreateWarning (99, Errors.MX0099, $"unknown abi: {target.Abis[0]}"));
break;
}
} else if (target.Abis.Count == 2 && target.Is32Build && target.Abis.Contains (Abi.ARMv7) && target.Abis.Contains (Abi.ARMv7s)) {
// We know we won't be running on arm64 if we're building for armv7+armv7s.
is_arm64_calling_convention = false;
}
}
}
public override SubStepTargets Targets {
get { return SubStepTargets.Assembly | SubStepTargets.Type | SubStepTargets.Method; }
}
public override bool IsActiveFor (AssemblyDefinition assembly)
{
// we're sure "pure" SDK assemblies don't use XamMac.dll (i.e. they are the Product assemblies)
if (Profile.IsSdkAssembly (assembly)) {
#if DEBUG
Console.WriteLine ("Assembly {0} : skipped (SDK)", assembly);
#endif
return false;
}
// process only assemblies where the linker is enabled (e.g. --linksdk, --linkskip)
AssemblyAction action = Annotations.GetAction (assembly);
if (action != AssemblyAction.Link) {
#if DEBUG
Console.WriteLine ("Assembly {0} : skipped ({1})", assembly, action);
#endif
return false;
}
// if the assembly does not refer to [CompilerGeneratedAttribute] then there's not much we can do
HasOptimizableCode = false;
foreach (TypeReference tr in assembly.MainModule.GetTypeReferences ()) {
if (tr.Is (Namespaces.ObjCRuntime, "BindingImplAttribute")) {
HasOptimizableCode = true;
break;
}
if (!Driver.IsXAMCORE_4_0 && tr.Is ("System.Runtime.CompilerServices", "CompilerGeneratedAttribute")) {
#if DEBUG
Console.WriteLine ("Assembly {0} : processing", assembly);
#endif
HasOptimizableCode = true;
break;
}
}
#if DEBUG
if (!HasOptimizableCode)
Console.WriteLine ("Assembly {0} : no [CompilerGeneratedAttribute] nor [BindingImplAttribute] present (applying basic optimizations)", assembly);
#endif
// we always apply the step
return true;
}
protected override void Process (TypeDefinition type)
{
if (!HasOptimizableCode)
return;
isdirectbinding_constant = type.IsNSObject (LinkContext) ? type.GetIsDirectBindingConstant (LinkContext) : null;
// if 'type' inherits from NSObject inside an assembly that has [GeneratedCode]
// or for static types used for optional members (using extensions methods), they can be optimized too
IsExtensionType = type.IsSealed && type.IsAbstract && type.Name.EndsWith ("_Extensions", StringComparison.Ordinal);
}
// [GeneratedCode] is not enough - e.g. it's used for anonymous delegates even if the
// code itself is not tool/compiler generated
static protected bool IsExport (ICustomAttributeProvider provider)
{
return provider.HasCustomAttribute (Namespaces.Foundation, "ExportAttribute");
}
// less risky to nop-ify if branches are pointing to this instruction
static protected void Nop (Instruction ins)
{
ins.OpCode = OpCodes.Nop;
ins.Operand = null;
}
protected static bool ValidateInstruction (MethodDefinition caller, Instruction ins, string operation, Code expected)
{
if (ins.OpCode.Code != expected) {
Driver.Log (1, "Could not {0} in {1} at offset {2}, expected {3} got {4}", operation, caller, ins.Offset, expected, ins);
return false;
}
return true;
}
protected static bool ValidateInstruction (MethodDefinition caller, Instruction ins, string operation, params Code [] expected)
{
foreach (var code in expected) {
if (ins.OpCode.Code == code)
return true;
}
Driver.Log (1, "Could not {0} in {1} at offset {2}, expected any of [{3}] got {4}", operation, caller, ins.Offset, string.Join (", ", expected), ins);
return false;
}
static int? GetConstantValue (Instruction ins)
{
if (ins == null)
return null;
switch (ins.OpCode.Code) {
case Code.Ldc_I4_0:
return 0;
case Code.Ldc_I4_1:
return 1;
case Code.Ldc_I4_2:
return 2;
case Code.Ldc_I4_3:
return 3;
case Code.Ldc_I4_4:
return 4;
case Code.Ldc_I4_5:
return 5;
case Code.Ldc_I4_6:
return 6;
case Code.Ldc_I4_7:
return 7;
case Code.Ldc_I4_8:
return 8;
case Code.Ldc_I4:
case Code.Ldc_I4_S:
if (ins.Operand is int)
return (int) ins.Operand;
if (ins.Operand is sbyte)
return (sbyte) ins.Operand;
return null;
#if DEBUG
case Code.Isinst: // We might be able to calculate a constant value here in the future
case Code.Ldloc:
case Code.Ldloca:
case Code.Ldloc_0:
case Code.Ldloc_1:
case Code.Ldloc_2:
case Code.Ldloc_3:
case Code.Ldloc_S:
case Code.Ldloca_S:
case Code.Ldarg:
case Code.Ldarg_0:
case Code.Ldarg_1:
case Code.Ldarg_2:
case Code.Ldarg_3:
case Code.Ldarg_S:
case Code.Ldarga:
case Code.Call:
case Code.Calli:
case Code.Callvirt:
case Code.Box:
case Code.Ldsfld:
case Code.Dup: // You might think we could get the constant of the previous instruction, but this instruction might be the target of a branch, in which case the question becomes: which instruction was the previous instruction? And that's not a question easily answered without a much more thorough analysis of the code.
case Code.Ldlen:
case Code.Ldind_U1:
case Code.Ldind_U2:
case Code.Ldind_U4:
case Code.Ldind_Ref:
case Code.Conv_I:
case Code.Conv_I1:
case Code.Conv_I2:
case Code.Conv_I4:
case Code.Conv_U:
case Code.Sizeof:
case Code.Ldfld:
case Code.Ldflda:
return null; // just to not hit the CWL below
#endif
default:
#if DEBUG
Driver.Log (9, "Unknown conditional instruction: {0}", ins);
#endif
return null;
}
}
static bool MarkInstructions (MethodDefinition method, Mono.Collections.Generic.Collection<Instruction> instructions, bool [] reachable, int start, int end)
{
if (reachable [start])
return true; // We've already marked this section of code
for (int i = start; i < end; i++) {
reachable [i] = true;
var ins = instructions [i];
switch (ins.OpCode.FlowControl) {
case FlowControl.Branch:
// Unconditional branch, we continue marking from the instruction that we branch to.
var br_target = (Instruction) ins.Operand;
return MarkInstructions (method, instructions, reachable, instructions.IndexOf (br_target), end);
case FlowControl.Cond_Branch:
// Conditional instruction, we need to check if we can calculate a constant value for the condition
var cond_target = ins.Operand as Instruction;
bool? branch = null; // did we get a constant value for the condition, and if so, did we branch or not?
var cond_instruction_count = 0; // The number of instructions that compose the condition
if (ins.OpCode.Code == Code.Switch) {
// Treat all branches of the switch statement as reachable.
// FIXME: calculate the potential constant branch (currently there are no optimizable methods where the switch condition is constant, so this is not needed for now)
var targets = ins.Operand as Instruction [];
if (targets == null) {
Driver.Log (4, $"Can't optimize {0} because of unknown target of branch instruction {1} {2}", method, ins, ins.Operand);
return false;
}
foreach (var target in targets) {
// not constant, continue marking both this code sequence and the branched sequence
if (!MarkInstructions (method, instructions, reachable, instructions.IndexOf (target), end))
return false;
}
return MarkInstructions (method, instructions, reachable, instructions.IndexOf (ins.Next), end);
}
if (cond_target == null) {
Driver.Log (4, $"Can't optimize {0} because of unknown target of branch instruction {1} {2}", method, ins, ins.Operand);
return false;
}
switch (ins.OpCode.Code) {
case Code.Brtrue:
case Code.Brtrue_S: {
var v = GetConstantValue (ins?.Previous);
if (v.HasValue)
branch = v.Value != 0;
cond_instruction_count = 2;
break;
}
case Code.Brfalse:
case Code.Brfalse_S: {
var v = GetConstantValue (ins?.Previous);
if (v.HasValue)
branch = v.Value == 0;
cond_instruction_count = 2;
break;
}
case Code.Beq:
case Code.Beq_S: {
var x1 = GetConstantValue (ins?.Previous?.Previous);
var x2 = GetConstantValue (ins?.Previous);
if (x1.HasValue && x2.HasValue)
branch = x1.Value == x2.Value;
cond_instruction_count = 3;
break;
}
case Code.Bne_Un:
case Code.Bne_Un_S: {
var x1 = GetConstantValue (ins?.Previous?.Previous);
var x2 = GetConstantValue (ins?.Previous);
if (x1.HasValue && x2.HasValue)
branch = x1.Value != x2.Value;
cond_instruction_count = 3;
break;
}
case Code.Ble:
case Code.Ble_S:
case Code.Ble_Un:
case Code.Ble_Un_S: {
var x1 = GetConstantValue (ins?.Previous?.Previous);
var x2 = GetConstantValue (ins?.Previous);
if (x1.HasValue && x2.HasValue)
branch = x1.Value <= x2.Value;
cond_instruction_count = 3;
break;
}
case Code.Blt:
case Code.Blt_S:
case Code.Blt_Un:
case Code.Blt_Un_S: {
var x1 = GetConstantValue (ins?.Previous?.Previous);
var x2 = GetConstantValue (ins?.Previous);
if (x1.HasValue && x2.HasValue)
branch = x1.Value < x2.Value;
cond_instruction_count = 3;
break;
}
case Code.Bge:
case Code.Bge_S:
case Code.Bge_Un:
case Code.Bge_Un_S: {
var x1 = GetConstantValue (ins?.Previous?.Previous);
var x2 = GetConstantValue (ins?.Previous);
if (x1.HasValue && x2.HasValue)
branch = x1.Value >= x2.Value;
cond_instruction_count = 3;
break;
}
case Code.Bgt:
case Code.Bgt_S:
case Code.Bgt_Un:
case Code.Bgt_Un_S: {
var x1 = GetConstantValue (ins?.Previous?.Previous);
var x2 = GetConstantValue (ins?.Previous);
if (x1.HasValue && x2.HasValue)
branch = x1.Value > x2.Value;
cond_instruction_count = 3;
break;
}
default:
Driver.Log ($"Can't optimize {0} because of unknown branch instruction: {1}", method, ins);
break;
}
if (branch.HasValue) {
// Make sure nothing else in the method branches into the middle of our supposedly constant condition,
// bypassing our constant calculation. Note that it's not a bad to branch to the _first_ instruction in
// the sequence (thus the +2 here), just into the middle of it.
if (AnyBranchTo (instructions, instructions [i - cond_instruction_count + 2], ins))
branch = null;
}
if (!branch.HasValue) {
// not constant, continue marking both this code sequence and the branched sequence
if (!MarkInstructions (method, instructions, reachable, instructions.IndexOf (cond_target), end))
return false;
} else {
// we can remove the branch (and the code that loads the condition), so we mark those instructions as dead.
for (int a = 0; a < cond_instruction_count; a++)
reachable [i - a] = false;
// Now continue marking according to whether we branched or not
if (branch.Value) {
// branch always taken
return MarkInstructions (method, instructions, reachable, instructions.IndexOf (cond_target), end);
} else {
// branch never taken
// continue looping
}
}
break;
case FlowControl.Call:
case FlowControl.Next:
// Nothing special, continue marking
break;
case FlowControl.Return:
case FlowControl.Throw:
// Control flow returns here, so stop marking
return true;
case FlowControl.Break:
case FlowControl.Meta:
case FlowControl.Phi:
default:
Driver.Log (4, "Can't optimize {0} because of unknown flow control for: {1}", method, ins);
return false;
}
}
return true;
}
// Check if there are any branches in the instructions that branch to anywhere between 'first' and 'last' instructions (both inclusive).
static bool AnyBranchTo (Mono.Collections.Generic.Collection<Instruction> instructions, Instruction first, Instruction last)
{
if (first.Offset > last.Offset) {
Driver.Log ($"Broken assumption: {first} is after {last}");
return true; // This is the safe thing to do, since it will prevent inlining
}
for (int i = 0; i < instructions.Count; i++) {
var ins = instructions [i];
switch (ins.OpCode.FlowControl) {
case FlowControl.Branch:
case FlowControl.Cond_Branch:
var target = ins.Operand as Instruction;
if (target != null && target.Offset >= first.Offset && target.Offset <= last.Offset)
return true;
break;
}
}
return false;
}
protected void EliminateDeadCode (MethodDefinition caller)
{
if (Optimizations.DeadCodeElimination != true)
return;
var instructions = caller.Body.Instructions;
var reachable = new bool [instructions.Count];
// We walk the instructions in the method, starting with the first instruction,
// marking all reachable instructions. Any non-reachable instructions at the end
// can be removed.
if (!MarkInstructions (caller, instructions, reachable, 0, instructions.Count))
return;
// Handle exception handlers specially, they do not follow normal code flow.
bool[] reachableExceptionHandlers = null;
if (caller.Body.HasExceptionHandlers) {
reachableExceptionHandlers = new bool [caller.Body.ExceptionHandlers.Count];
for (var e = 0; e < reachableExceptionHandlers.Length; e++) {
var eh = caller.Body.ExceptionHandlers [e];
// First check if the protected region is reachable
var startI = instructions.IndexOf (eh.TryStart);
var endI = instructions.IndexOf (eh.TryEnd);
for (int i = startI; i < endI; i++) {
if (reachable [i]) {
reachableExceptionHandlers [e] = true;
break;
}
}
// The protected code isn't reachable, none of the handlers will be executed
if (!reachableExceptionHandlers [e])
continue;
switch (eh.HandlerType) {
case ExceptionHandlerType.Catch:
// We don't need catch handlers the reachable instructions are all nops
var allNops = true;
for (int i = startI; i < endI; i++) {
if (instructions [i].OpCode.Code != Code.Nop) {
allNops = false;
break;
}
}
if (!allNops) {
if (!MarkInstructions (caller, instructions, reachable, instructions.IndexOf (eh.HandlerStart), instructions.IndexOf (eh.HandlerEnd)))
return;
}
break;
case ExceptionHandlerType.Finally:
// finally clauses are always executed, even if the protected region is empty
if (!MarkInstructions (caller, instructions, reachable, instructions.IndexOf (eh.HandlerStart), instructions.IndexOf (eh.HandlerEnd)))
return;
break;
case ExceptionHandlerType.Fault:
case ExceptionHandlerType.Filter:
// FIXME: and until fixed, exit gracefully without doing anything
Driver.Log (4, "Unhandled exception handler: {0}, skipping dead code elimination for {1}", eh.HandlerType, caller);
return;
}
}
}
if (Array.IndexOf (reachable, false) == -1)
return; // entire method is reachable
// Kill branch instructions when there are only dead instructions between the branch instruction and the target of the branch
for (int i = 0; i < instructions.Count; i++) {
if (!reachable [i])
continue;
var ins = instructions [i];
if (ins.OpCode.Code != Code.Br && ins.OpCode.Code != Code.Br_S)
continue;
var target = ins.Operand as Instruction;
if (target == null)
continue;
if (target.Offset < ins.Offset)
continue; // backwards branch, keep those
var start = i + 1;
var end = instructions.IndexOf (target);
var any_reachable = false;
for (int k = start; k < end; k++) {
if (reachable [k]) {
any_reachable = true;
break;
}
}
if (any_reachable)
continue;
// The branch instruction just branches over unreachable instructions, so it can be considered unreachable too.
reachable [i] = false;
}
// Check if there are unreachable instructions at the end.
var last_reachable = Array.LastIndexOf (reachable, true);
if (last_reachable < reachable.Length - 1) {
// There are unreachable instructions at the end.
// We must verify that there are no branches into these instructions.
// In theory there shouldn't be any (if there are branches into these instructions,
// they're reachable), but let's still verify just in case.
var last_reachable_offset = instructions [last_reachable].Offset;
for (int i = 0; i < last_reachable; i++) {
if (!reachable [i])
continue; // Unreachable instructions don't branch anywhere, because they'll be removed.
var ins = instructions [i];
switch (ins.OpCode.FlowControl) {
case FlowControl.Break:
case FlowControl.Cond_Branch:
var target = (Instruction) ins.Operand;
if (target.Offset > last_reachable_offset) {
Driver.Log (4, "Can't optimize {0} because of branching beyond last instruction alive: {1}", caller, ins);
return;
}
break;
}
}
}
#if false
Console.WriteLine ($"{caller.FullName}:");
for (int i = 0; i < reachable.Length; i++) {
Console.WriteLine ($"{(reachable [i] ? " " : "- ")} {instructions [i]}");
if (!reachable [i])
Nop (instructions [i]);
}
Console.WriteLine ();
#endif
// Exterminate, exterminate, exterminate
for (int i = 0; i < reachable.Length; i++) {
if (!reachable [i])
Nop (instructions [i]);
}
// Remove exception handlers
if (reachableExceptionHandlers != null) {
for (int i = reachableExceptionHandlers.Length - 1; i >= 0; i--) {
if (reachableExceptionHandlers [i])
continue;
caller.Body.ExceptionHandlers.RemoveAt (i);
}
}
// Remove unreachable instructions (nops) at the end, because the last instruction can only be ret/throw/backwards branch.
for (int i = last_reachable + 1; i < reachable.Length; i++)
instructions.RemoveAt (last_reachable + 1);
}
protected override void Process (AssemblyDefinition assembly)
{
// The "get_Size" is a performance (over size) optimization.
// It always makes sense for platform assemblies because:
// * Xamarin.TVOS.dll only ship the 64 bits code paths (all 32 bits code is extra weight better removed)
// * Xamarin.WatchOS.dll only ship the 32 bits code paths (all 64 bits code is extra weight better removed)
// * Xamarin.iOS.dll ship different 32/64 bits versions of the assembly anyway (nint... support)
// Each is better to be optimized (it will be smaller anyway)
//
// However for fat (32/64) apps (i.e. iOS only right now) the optimization can duplicate the assembly
// (metadata) for 3rd parties binding projects, increasing app size for very minimal performance gains.
// For non-fat apps (the AppStore allows 64bits only iOS apps) then it's better to be applied
//
// TODO: we could make this an option "optimize for size vs optimize for speed" in the future
if (Optimizations.InlineIntPtrSize.HasValue) {
InlineIntPtrSize = Optimizations.InlineIntPtrSize.Value;
} else if (!IsDualBuild) {
InlineIntPtrSize = true;
} else {
InlineIntPtrSize = (Profile.Current as BaseProfile).ProductAssembly == assembly.Name.Name;
}
Driver.Log (4, "Optimization 'inline-intptr-size' enabled for assembly '{0}'.", assembly.Name);
base.Process (assembly);
}
protected override void Process (MethodDefinition method)
{
if (!method.HasBody)
return;
if (method.IsBindingImplOptimizableCode (LinkContext)) {
// We optimize all methods that have the [BindingImpl (BindingImplAttributes.Optimizable)] attribute.
} else if (!Driver.IsXAMCORE_4_0 && (method.IsGeneratedCode (LinkContext) && (IsExtensionType || IsExport (method)))) {
// We optimize methods that have the [GeneratedCodeAttribute] and is either an extension type or an exported method
} else {
// but it would be too risky to apply on user-generated code
return;
}
if (!LinkContext.App.DynamicRegistrationSupported && method.Name == "get_DynamicRegistrationSupported" && method.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) {
// Rewrite to return 'false'
var instr = method.Body.Instructions;
instr.Clear ();
instr.Add (Instruction.Create (OpCodes.Ldc_I4_0));
instr.Add (Instruction.Create (OpCodes.Ret));
return; // nothing else to do here.
}
if (Optimizations.InlineIsARM64CallingConvention == true && is_arm64_calling_convention.HasValue && method.Name == "GetIsARM64CallingConvention" && method.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) {
// Rewrite to return the constant value
var instr = method.Body.Instructions;
instr.Clear ();
instr.Add (Instruction.Create (is_arm64_calling_convention.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0));
instr.Add (Instruction.Create (OpCodes.Ret));
return; // nothing else to do here.
}
var instructions = method.Body.Instructions;
for (int i = 0; i < instructions.Count; i++) {
var ins = instructions [i];
switch (ins.OpCode.Code) {
case Code.Call:
i += ProcessCalls (method, ins);
break;
case Code.Ldsfld:
ProcessLoadStaticField (method, ins);
break;
}
}
EliminateDeadCode (method);
}
// Returns the number of instructions add (or removed).
protected virtual int ProcessCalls (MethodDefinition caller, Instruction ins)
{
var mr = ins.Operand as MethodReference;
switch (mr?.Name) {
case "EnsureUIThread":
ProcessEnsureUIThread (caller, ins);
break;
case "get_Size":
ProcessIntPtrSize (caller, ins);
break;
case "get_IsDirectBinding":
ProcessIsDirectBinding (caller, ins);
break;
case "get_DynamicRegistrationSupported":
ProcessIsDynamicSupported (caller, ins);
break;
case "SetupBlock":
case "SetupBlockUnsafe":
return ProcessSetupBlock (caller, ins);
}
return 0;
}
protected virtual void ProcessLoadStaticField (MethodDefinition caller, Instruction ins)
{
FieldReference fr = ins.Operand as FieldReference;
switch (fr?.Name) {
case "IsARM64CallingConvention":
ProcessIsARM64CallingConvention (caller, ins);
break;
case "Arch":
// https://app.asana.com/0/77259014252/77812690163
ProcessRuntimeArch (caller, ins);
break;
}
}
void ProcessEnsureUIThread (MethodDefinition caller, Instruction ins)
{
#if MONOTOUCH
const string operation = "remove calls to UIApplication::EnsureUIThread";
#else
const string operation = "remove calls to NSApplication::EnsureUIThread";
#endif
if (Optimizations.RemoveUIThreadChecks != true)
return;
// Verify we're checking the right get_EnsureUIThread call
var mr = ins.Operand as MethodReference;
#if MONOTOUCH
if (!mr.DeclaringType.Is (Namespaces.UIKit, "UIApplication"))
return;
#else
if (!mr.DeclaringType.Is (Namespaces.AppKit, "NSApplication"))
return;
#endif
// Verify a few assumptions before doing anything
if (!ValidateInstruction (caller, ins, operation, Code.Call))
return;
// This is simple: just remove the call
Nop (ins); // call void UIKit.UIApplication::EnsureUIThread()
}
void ProcessIntPtrSize (MethodDefinition caller, Instruction ins)
{
if (!InlineIntPtrSize)
return;
// This will inline IntPtr.Size to load the corresponding constant value instead
// Verify we're checking the right get_Size call
var mr = ins.Operand as MethodReference;
if (!mr.DeclaringType.Is ("System", "IntPtr"))
return;
// We're fine, inline the get_Size call
ins.OpCode = Arch == 8 ? OpCodes.Ldc_I4_8 : OpCodes.Ldc_I4_4;
ins.Operand = null;
}
void ProcessIsDirectBinding (MethodDefinition caller, Instruction ins)
{
const string operation = "inline IsDirectBinding";
if (Optimizations.InlineIsDirectBinding != true)
return;
// If we don't know the constant isdirectbinding value, then we can't inline anything
if (!isdirectbinding_constant.HasValue)
return;
// Verify we're checking the right get_IsDirectBinding call
var mr = ins.Operand as MethodReference;
if (!mr.DeclaringType.Is (Namespaces.Foundation, "NSObject"))
return;
// Verify a few assumptions before doing anything
if (!ValidateInstruction (caller, ins.Previous, operation, Code.Ldarg_0))
return;
if (!ValidateInstruction (caller, ins, operation, Code.Call))
return;
// Clearing the branch succeeded, so clear the condition too
// ldarg.0
Nop (ins.Previous);
// call System.Boolean Foundation.NSObject::get_IsDirectBinding()
ins.OpCode = isdirectbinding_constant.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0;
ins.Operand = null;
}
void ProcessIsDynamicSupported (MethodDefinition caller, Instruction ins)
{
const string operation = "inline Runtime.DynamicRegistrationSupported";
if (Optimizations.InlineDynamicRegistrationSupported != true)
return;
// Verify we're checking the right Runtime.IsDynamicSupported call
var mr = ins.Operand as MethodReference;
if (!mr.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime"))
return;
if (!ValidateInstruction (caller, ins, operation, Code.Call))
return;
// We're fine, inline the Runtime.IsDynamicSupported condition
ins.OpCode = LinkContext.App.DynamicRegistrationSupported ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0;
ins.Operand = null;
}
int ProcessSetupBlock (MethodDefinition caller, Instruction ins)
{
if (Optimizations.OptimizeBlockLiteralSetupBlock != true)
return 0;
// This will optimize calls to SetupBlock and SetupBlockUnsafe by calculating the signature for the block
// (which both SetupBlock and SetupBlockUnsafe do), and then rewrite the code to call SetupBlockImpl instead
// (which takes the block signature as an argument instead of calculating it). This is required to
// remove the dynamic registrar, because calculating the block signature is done in the dynamic registrar.
//
// This code is a mirror of the code in BlockLiteral.SetupBlock (to calculate the block signature).
var mr = ins.Operand as MethodReference;
if (!mr.DeclaringType.Is (Namespaces.ObjCRuntime, "BlockLiteral"))
return 0;
if (caller.Name == "GetBlockForDelegate" && caller.DeclaringType.Is ("ObjCRuntime", "BlockLiteral"))
return 0; // BlockLiteral.GetBlockForDelegate contains a non-optimizable call to SetupBlock, and this way we don't show any warnings to users about things they can't do anything about.
string signature = null;
try {
// We need to figure out the type of the first argument to the call to SetupBlock[Impl].
//
// Example sequence:
//
// ldsfld ObjCRuntime.Trampolines/DJSContextExceptionHandler ObjCRuntime.Trampolines/SDJSContextExceptionHandler::Handler
// ldarg.1
// call System.Void ObjCRuntime.BlockLiteral::SetupBlockUnsafe(System.Delegate, System.Delegate)
//
// Locating the instruction that loads the first argument can be complicated, so we simplify by making a few assumptions:
// 1. The instruction immediately before the call instruction (which would load the last argument) is a Push1/Pop0 instruction.
// This avoids running into trouble when the instruction does something else (it could be a any other instruction, which would throw off the next calculations)
// 2. We have a whitelist of instructions we know how to calculate the type for, and which we use on the second to last instruction before the call instruction
// First verify the Push1/Pop0 behavior in point 1.
var prev = ins.Previous;
while (prev.OpCode.Code == Code.Nop)
prev = prev.Previous; // Skip any nops.
if (prev.OpCode.StackBehaviourPush != StackBehaviour.Push1) {
//todo: localize mmp error 2106
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 2106, caller, ins, Errors.MM2106, caller, ins.Offset, mr.Name, prev));
return 0;
} else if (prev.OpCode.StackBehaviourPop != StackBehaviour.Pop0) {
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 2106, caller, ins, Errors.MM2106, caller, ins.Offset, mr.Name, prev));
return 0;
}
var loadTrampolineInstruction = prev.Previous;
while (loadTrampolineInstruction.OpCode.Code == Code.Nop)
loadTrampolineInstruction = loadTrampolineInstruction.Previous; // Skip any nops.
// Then find the type of the previous instruction (the first argument to SetupBlock[Unsafe])
var trampolineDelegateType = GetPushedType (caller, loadTrampolineInstruction);
if (trampolineDelegateType == null) {
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 2106, caller, ins, Errors.MM2106_A, caller, ins.Offset, mr.Name, loadTrampolineInstruction));
return 0;
}
if (trampolineDelegateType.Is ("System", "Delegate") || trampolineDelegateType.Is ("System", "MulticastDelegate")) {
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 2106, caller, ins, Errors.MM2106_B, caller, trampolineDelegateType.FullName, mr.Name));
return 0;
}
// Calculate the block signature.
var blockSignature = false;
MethodReference userMethod = null;
// First look for any [UserDelegateType] attributes on the trampoline delegate type.
var userDelegateType = GetUserDelegateType (trampolineDelegateType);
if (userDelegateType != null) {
var userMethodDefinition = GetDelegateInvoke (userDelegateType);
userMethod = InflateMethod (userDelegateType, userMethodDefinition);
blockSignature = true;
} else {
// Couldn't find a [UserDelegateType] attribute, use the type of the actual trampoline instead.
var userMethodDefinition = GetDelegateInvoke (trampolineDelegateType);
userMethod = InflateMethod (trampolineDelegateType, userMethodDefinition);
blockSignature = false;
}
// No luck finding the signature, so give up.
if (userMethod == null) {
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 2106, caller, ins, Errors.MM2106_C, caller, ins.Offset, trampolineDelegateType.FullName));
return 0;
}
var parameters = new TypeReference [userMethod.Parameters.Count];
for (int p = 0; p < parameters.Length; p++)
parameters [p] = userMethod.Parameters [p].ParameterType;
signature = LinkContext.Target.StaticRegistrar.ComputeSignature (userMethod.DeclaringType, false, userMethod.ReturnType, parameters, userMethod.Resolve (), isBlockSignature: blockSignature);
} catch (Exception e) {
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 2106, e, caller, ins, Errors.MM2106_D, caller, ins.Offset, e.Message));
return 0;
}
// We got the information we need: rewrite the IL.
var instructions = caller.Body.Instructions;
var index = instructions.IndexOf (ins);
// Inject the extra arguments
instructions.Insert (index, Instruction.Create (OpCodes.Ldstr, signature));
instructions.Insert (index, Instruction.Create (mr.Name == "SetupBlockUnsafe" ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1));
// Change the call to call SetupBlockImpl instead
ins.Operand = GetBlockSetupImpl (caller, ins);
//Driver.Log (4, "Optimized call to BlockLiteral.SetupBlock in {0} at offset {1} with delegate type {2} and signature {3}", caller, ins.Offset, delegateType.FullName, signature);
return 2;
}
int ProcessIsARM64CallingConvention (MethodDefinition caller, Instruction ins)
{
const string operation = "inline Runtime.IsARM64CallingConvention";
if (Optimizations.InlineIsARM64CallingConvention != true)
return 0;
if (!is_arm64_calling_convention.HasValue)
return 0;
// Verify we're checking the right IsARM64CallingConvention field
var fr = ins.Operand as FieldReference;
if (!fr.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime"))
return 0;
if (!ValidateInstruction (caller, ins, operation, Code.Ldsfld))
return 0;
// We're fine, inline the Runtime.IsARM64CallingConvention value
ins.OpCode = is_arm64_calling_convention.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0;
ins.Operand = null;
return 0;
}
void ProcessRuntimeArch (MethodDefinition caller, Instruction ins)
{
const string operation = "inline Runtime.Arch";
if (Optimizations.InlineRuntimeArch != true)
return;
// Verify we're checking the right Arch field
var fr = ins.Operand as FieldReference;
if (!fr.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime"))
return;
// Verify a few assumptions before doing anything
if (!ValidateInstruction (caller, ins, operation, Code.Ldsfld))
return;
// We're fine, inline the Runtime.Arch condition
// The enum values are Runtime.DEVICE = 0 and Runtime.SIMULATOR = 1,
ins.OpCode = Device ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1;
ins.Operand = null;
}
// Returns the type of the value pushed on the stack by the given instruction.
// Returns null for unknown instructions, or for instructions that don't push anything on the stack.
TypeReference GetPushedType (MethodDefinition method, Instruction ins)
{
var index = 0;
switch (ins.OpCode.Code) {
case Code.Ldloc_0:
case Code.Ldarg_0:
index = 0;
break;
case Code.Ldloc_1:
case Code.Ldarg_1:
index = 1;
break;
case Code.Ldloc_2:
case Code.Ldarg_2:
index = 2;
break;
case Code.Ldloc_3:
case Code.Ldarg_3:
index = 3;
break;
case Code.Ldloc:
case Code.Ldloc_S:
return ((VariableDefinition) ins.Operand).VariableType;
case Code.Ldarg:
case Code.Ldarg_S:
return ((ParameterDefinition) ins.Operand).ParameterType;
case Code.Ldfld:
case Code.Ldsfld: