Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VB Compiler Scanner] Perf Tweaks #6794

Closed

Conversation

AdamSpeight2008
Copy link
Contributor

This PR implements the following set of changes.

  • Change IntegralLiteralCharacterValue to use a dictionary lookup.
  • Tweaks to conditionals.
  • Remove redundant / unreachable code.
  • Simplify a couple of array initialisations. eg DaysToMonth365(13-1) to DaysToMonth365(12)

Change `IntegralLiteralCharacterValue` to use a dictionary lookup.
@gafter
Copy link
Member

gafter commented Nov 16, 2015

Please remove all the tweaks aside from the principal change to a dictionary lookup.

Please provide detailed performance comparisons on the changed code under realistic distributions of argument values. We need evidence this makes things better, not worse. Why is a dictionary better than an array?

@AdamSpeight2008
Copy link
Contributor Author

@gafter

  • All the changes are for perf. They maybe small gains but it adds up.
  • The array would be too big,
  • The values have been precomputed, no more Casts
  • The combined IL size is smaller.

Test Reports

32bit Scanner Tests
Test# 1212 3.0 sec
Test# 1209 2.5 sec (Is from #6641 )

64bit Scanner Tests
Test# 1199 4.0sec
Test# 1196 4.3sec (is from #6641)

I believe that it should be possible to get 64bit near 3.0 sec.

@AdamSpeight2008 AdamSpeight2008 changed the title [VB Compiler Scanner] Tweaks [VB Compiler Scanner] Perf Tweaks Nov 16, 2015
@gafter
Copy link
Member

gafter commented Nov 16, 2015

OK, so 64-bit is only a little bit worse with your change?

These performance tests that you are quoting appear to be far too short in duration for single-run test results to lead to any conclusions. I think the improvements would need to be demonstrated in a custom test for this purpose.

When you say "all the changes are for perf", can you please explain how changing

        Private Const s_fullwidth = CInt(&HFF00L - &H0020L)

        Friend Const CHARACTER_TABULATION As Char = ChrW(&H0009)
        Friend Const LINE_FEED As Char = ChrW(&H000A)
        Friend Const CARRIAGE_RETURN As Char = ChrW(&H000D)
        Friend Const SPACE As Char = ChrW(&H0020)

to

        Private Const s_fullwidth = &HFEE0

        Friend Const CHARACTER_TABULATION = ChrW(&H0009)
        Friend Const LINE_FEED = ChrW(&H000A)
        Friend Const CARRIAGE_RETURN = ChrW(&H000D)
        Friend Const SPACE = ChrW(&H0020)

(or scores of other similar changes) improves the perf?

@mikedn
Copy link

mikedn commented Nov 17, 2015

There's little chance that changing OrElse and AndAlso to Or and And would be improvement. It may work in certain cases but in the general case is a de-optimization. You'd need to analyze each such change you did and show that it is an improvement.

The use of a hashtable is debatable. Everyone knows that hashtable lookup has constant time complexity but that's only the theory. In practice there's a good chance that that constant time is longer than the time taken by the few Ifs the hashtable replaces.

@AdamSpeight2008
Copy link
Contributor Author

IntegralLiteralCharacterValue

Current

.method assembly static 
    uint8 IntegralLiteralCharacterValue (
        char Digit
    ) cil managed 
{
    // Method begins at RVA 0x1cbf10
    // Code size 109 (0x6d)
    .maxstack 2
    .locals init (
        [0] uint8,
        [1] int32,
        [2] bool,
        [3] bool,
        [4] bool
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: call bool Microsoft.CodeAnalysis.VisualBasic.SyntaxFacts::IsFullWidth(char)
    IL_0007: stloc.2
    IL_0008: ldloc.2
    IL_0009: brfalse.s IL_0014
    IL_000b: ldarg.0
    IL_000c: call char Microsoft.CodeAnalysis.VisualBasic.SyntaxFacts::MakeHalfWidth(char)
    IL_0011: starg.s Digit
    IL_0013: nop
    IL_0014: nop
    IL_0015: ldarg.0
    IL_0016: stloc.1
    IL_0017: ldarg.0
    IL_0018: call bool Microsoft.CodeAnalysis.VisualBasic.SyntaxFacts::IsDecimalDigit(char)
    IL_001d: stloc.3
    IL_001e: ldloc.3
    IL_001f: brfalse.s IL_0029
    IL_0021: ldloc.1
    IL_0022: ldc.i4.s 48
    IL_0024: sub
    IL_0025: conv.u1
    IL_0026: stloc.0
    IL_0027: br.s IL_006b
    IL_0029: ldarg.0
    IL_002a: ldc.i4.s 65
    IL_002c: blt.s IL_0038
    IL_002e: ldarg.0
    IL_002f: ldc.i4.s 70
    IL_0031: cgt
    IL_0033: ldc.i4.0
    IL_0034: ceq
    IL_0036: br.s IL_0039
    IL_0038: ldc.i4.0
    IL_0039: stloc.s 4
    IL_003b: ldloc.s 4
    IL_003d: brfalse.s IL_0047
    IL_003f: ldloc.1
    IL_0040: ldc.i4.s -55
    IL_0042: add
    IL_0043: conv.u1
    IL_0044: stloc.0
    IL_0045: br.s IL_006b
    IL_0047: nop
    IL_0048: ldarg.0
    IL_0049: ldc.i4.s 97
    IL_004b: blt.s IL_0057
    IL_004d: ldarg.0
    IL_004e: ldc.i4.s 102
    IL_0050: cgt
    IL_0052: ldc.i4.0
    IL_0053: ceq
    IL_0055: br.s IL_0058
    IL_0057: ldc.i4.0
    IL_0058: ldstr "Surprising digit."
    IL_005d: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::Assert(bool, string)
    IL_0062: nop
    IL_0063: ldloc.1
    IL_0064: ldc.i4.s -87
    IL_0066: add
    IL_0067: conv.u1
    IL_0068: stloc.0
    IL_0069: br.s IL_006b
    IL_006b: ldloc.0
    IL_006c: ret
} // end of method SyntaxFacts::IntegralLiteralCharacterValue

PR

.method assembly static 
    uint8 IntegralLiteralCharacterValue (
        char Digit
    ) cil managed 
{
    // Method begins at RVA 0x1d1b18
    // Code size 41 (0x29)
    .maxstack 3
    .locals init (
        [0] uint8,
        [1] uint8,
        [2] bool
    )

    IL_0000: nop
    IL_0001: ldsfld class [System.Collections]System.Collections.Generic.Dictionary`2<char, uint8> Microsoft.CodeAnalysis.VisualBasic.SyntaxFacts::_CharacterValues_
    IL_0006: ldarg.0
    IL_0007: ldloca.s 1
    IL_0009: callvirt instance bool class [System.Collections]System.Collections.Generic.Dictionary`2<char, uint8>::TryGetValue(!0, !1&)
    IL_000e: ldc.i4.0
    IL_000f: ceq
    IL_0011: stloc.2
    IL_0012: ldloc.2
    IL_0013: brfalse.s IL_0022
    IL_0015: ldc.i4.0
    IL_0016: ldstr "Surprising digit."
    IL_001b: call void [System.Diagnostics.Debug]System.Diagnostics.Debug::Assert(bool, string)
    IL_0020: nop
    IL_0021: nop
    IL_0022: nop
    IL_0023: ldloc.1
    IL_0024: stloc.0
    IL_0025: br.s IL_0027
    IL_0027: ldloc.0
    IL_0028: ret
} // end of method SyntaxFacts::IntegralLiteralCharacterValue


@mikedn
The And and OR versions have few branches, which help it to be jitted,
Plus are smaller in terms of number of IL instructions, which increases the chances being inlined by the JIT.
The process has been incremental, if it resulted improvement (I use the total test time ) it got kept.
(see #6411)

IsXmlWhitespace

Current

.method public static 
    bool IsXmlWhitespace (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1ce99c
    // Code size 36 (0x24)
    .maxstack 2
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 32
    IL_0003: ldarg.0
    IL_0004: beq.s IL_001e
    IL_0006: ldc.i4.s 9
    IL_0008: ldarg.0
    IL_0009: beq.s IL_001e
    IL_000b: ldarg.0
    IL_000c: ldc.i4 128
    IL_0011: ble.s IL_001b
    IL_0013: ldarg.0
    IL_0014: call bool [Microsoft.CodeAnalysis]Microsoft.CodeAnalysis.XmlCharType::IsWhiteSpace(char)
    IL_0019: br.s IL_001c
    IL_001b: ldc.i4.0
    IL_001c: br.s IL_001f
    IL_001e: ldc.i4.1
    IL_001f: stloc.0
    IL_0020: br.s IL_0022
    IL_0022: ldloc.0
    IL_0023: ret
} // end of method SyntaxFacts::IsXmlWhitespace

PR

.method public static 
    bool IsXmlWhitespace (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1d1908
    // Code size 33 (0x21)
    .maxstack 3
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 32
    IL_0003: ldarg.0
    IL_0004: ceq
    IL_0006: ldc.i4.s 9
    IL_0008: ldarg.0
    IL_0009: ceq
    IL_000b: or
    IL_000c: ldarg.0
    IL_000d: ldc.i4 128
    IL_0012: cgt
    IL_0014: ldarg.0
    IL_0015: call bool [Microsoft.CodeAnalysis]Microsoft.CodeAnalysis.XmlCharType::IsWhiteSpace(char)
    IL_001a: and
    IL_001b: or
    IL_001c: stloc.0
    IL_001d: br.s IL_001f
    IL_001f: ldloc.0
    IL_0020: ret
} // end of method SyntaxFacts::IsXmlWhitespace

Current

.method public static 
    bool IsWhitespace (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1ce96c
    // Code size 36 (0x24)
    .maxstack 2
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 32
    IL_0003: ldarg.0
    IL_0004: beq.s IL_001e
    IL_0006: ldc.i4.s 9
    IL_0008: ldarg.0
    IL_0009: beq.s IL_001e
    IL_000b: ldarg.0
    IL_000c: ldc.i4 128
    IL_0011: ble.s IL_001b
    IL_0013: ldarg.0
    IL_0014: call bool Microsoft.CodeAnalysis.VisualBasic.SyntaxFacts::IsWhitespaceNotAscii(char)
    IL_0019: br.s IL_001c
    IL_001b: ldc.i4.0
    IL_001c: br.s IL_001f
    IL_001e: ldc.i4.1
    IL_001f: stloc.0
    IL_0020: br.s IL_0022
    IL_0022: ldloc.0
    IL_0023: ret
} // end of method SyntaxFacts::IsWhitespace

IsWhitespace

PR

.method public static 
    bool IsWhitespace (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1d18d8
    // Code size 33 (0x21)
    .maxstack 3
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 32
    IL_0003: ldarg.0
    IL_0004: ceq
    IL_0006: ldc.i4.s 9
    IL_0008: ldarg.0
    IL_0009: ceq
    IL_000b: or
    IL_000c: ldarg.0
    IL_000d: ldc.i4 128
    IL_0012: cgt
    IL_0014: ldarg.0
    IL_0015: call bool Microsoft.CodeAnalysis.VisualBasic.SyntaxFacts::IsWhitespaceNotAscii(char)
    IL_001a: and
    IL_001b: or
    IL_001c: stloc.0
    IL_001d: br.s IL_001f
    IL_001f: ldloc.0
    IL_0020: ret
} // end of method SyntaxFacts::IsWhitespace

IsNewLIne

Current

.method public static 
    bool IsNewLine (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1cea1c
    // Code size 57 (0x39)
    .maxstack 2
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 13
    IL_0003: ldarg.0
    IL_0004: beq.s IL_0033
    IL_0006: ldc.i4.s 10
    IL_0008: ldarg.0
    IL_0009: beq.s IL_0033
    IL_000b: ldarg.0
    IL_000c: ldc.i4 133
    IL_0011: blt.s IL_0030
    IL_0013: ldc.i4 133
    IL_0018: ldarg.0
    IL_0019: beq.s IL_002d
    IL_001b: ldc.i4 8232
    IL_0020: ldarg.0
    IL_0021: beq.s IL_002d
    IL_0023: ldc.i4 8233
    IL_0028: ldarg.0
    IL_0029: ceq
    IL_002b: br.s IL_002e
    IL_002d: ldc.i4.1
    IL_002e: br.s IL_0031
    IL_0030: ldc.i4.0
    IL_0031: br.s IL_0034
    IL_0033: ldc.i4.1
    IL_0034: stloc.0
    IL_0035: br.s IL_0037
    IL_0037: ldloc.0
    IL_0038: ret
} // end of method SyntaxFacts::IsNewLine

PR

.method public static 
    bool IsNewLine (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1d1988
    // Code size 56 (0x38)
    .maxstack 5
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldc.i4.s 13
    IL_0003: ldarg.0
    IL_0004: ceq
    IL_0006: ldc.i4.s 10
    IL_0008: ldarg.0
    IL_0009: ceq
    IL_000b: or
    IL_000c: ldarg.0
    IL_000d: ldc.i4 133
    IL_0012: clt
    IL_0014: ldc.i4.0
    IL_0015: ceq
    IL_0017: ldc.i4 133
    IL_001c: ldarg.0
    IL_001d: ceq
    IL_001f: ldc.i4 8232
    IL_0024: ldarg.0
    IL_0025: ceq
    IL_0027: or
    IL_0028: ldc.i4 8233
    IL_002d: ldarg.0
    IL_002e: ceq
    IL_0030: or
    IL_0031: and
    IL_0032: or
    IL_0033: stloc.0
    IL_0034: br.s IL_0036
    IL_0036: ldloc.0
    IL_0037: ret
} // end of method SyntaxFacts::IsNewLine

IsSingleQuote

Current

.method assembly static 
    bool IsSingleQuote (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1cea64
    // Code size 51 (0x33)
    .maxstack 3
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.s 39
    IL_0004: beq.s IL_002d
    IL_0006: ldarg.0
    IL_0007: ldc.i4 8216
    IL_000c: blt.s IL_002a
    IL_000e: ldarg.0
    IL_000f: ldc.i4 65287
    IL_0014: ceq
    IL_0016: ldarg.0
    IL_0017: ldc.i4 8216
    IL_001c: ceq
    IL_001e: or
    IL_001f: ldarg.0
    IL_0020: ldc.i4 8217
    IL_0025: ceq
    IL_0027: or
    IL_0028: br.s IL_002b
    IL_002a: ldc.i4.0
    IL_002b: br.s IL_002e
    IL_002d: ldc.i4.1
    IL_002e: stloc.0
    IL_002f: br.s IL_0031
    IL_0031: ldloc.0
    IL_0032: ret
} // end of method SyntaxFacts::IsSingleQuote

PR

.method assembly static 
    bool IsSingleQuote (
        char c
    ) cil managed 
{
    // Method begins at RVA 0x1d19cc
    // Code size 50 (0x32)
    .maxstack 5
    .locals init (
        [0] bool
    )
    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.s 39
    IL_0004: ceq
    IL_0006: ldarg.0
    IL_0007: ldc.i4 8216
    IL_000c: clt
    IL_000e: ldc.i4.0
    IL_000f: ceq
    IL_0011: ldarg.0
    IL_0012: ldc.i4 65287
    IL_0017: ceq
    IL_0019: ldarg.0
    IL_001a: ldc.i4 8216
    IL_001f: ceq
    IL_0021: or
    IL_0022: ldarg.0
    IL_0023: ldc.i4 8217
    IL_0028: ceq
    IL_002a: or
    IL_002b: and
    IL_002c: or
    IL_002d: stloc.0
    IL_002e: br.s IL_0030
    IL_0030: ldloc.0
    IL_0031: ret
} // end of method SyntaxFacts::IsSingleQuote

@mikedn
Copy link

mikedn commented Nov 17, 2015

The And and OR versions have few branches, which help it to be jitted,

Helps how?

Plus are smaller in terms of number of IL instructions, which increases the chances being inlined by the JIT.

And what happens when the JIT compiler inline heuristics are changed and your IsWhitespace version is no longer inlined because the generated native code is too large?

I use the total test time

How is the total test time relevant to such micro optimizations considering that

  • the timing results have high variance
  • all these test most likely run on VMs
  • there may be many test cases that don't reflect real world scenarios

@AdamSpeight2008
Copy link
Contributor Author

@mikedn Suggested reading material. Just change&& to AndAlso and || to OrElse
Eric Lippert's Article

@mikedn
Copy link

mikedn commented Nov 17, 2015

That reading material is highly speculative. There are no timings, no real world examples, no analysis of the native code generated by the JIT compiler.

@gafter
Copy link
Member

gafter commented Nov 17, 2015

@AdamSpeight2008 we'll keep this PR open for another week to give you the time to put together a micro benchmark to justify the changes. Otherwise we'll shelve them until someone has the time to do it.

@davkean davkean added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Nov 18, 2015
@mikedn
Copy link

mikedn commented Nov 22, 2015

I've done a quick test of for IntegralLiteralCharacterValue (using 1 as test digit):

    Sub Test(ch As Char)
        Dim sum As Integer = IntegralLiteralCharacterValue(ch)
        Dim sw As Stopwatch = Stopwatch.StartNew()
        For i As Integer = 0 To 100000000
            sum += IntegralLiteralCharacterValue(ch)
        Next
        sw.Stop()
        Console.WriteLine("{0} {1}", sum, sw.ElapsedMilliseconds)
    End Sub
Time Description
1780ms Dictionary version
590ms Original version
620ms Original version + And/Or in IsDecimalDigit
650ms Original version + AndAlso in IsFullWidth
610ms Same as above + MethodImplOptions.AggressiveInlining for IsFullWidth
370ms Same as above + MethodImplOptions.AggressiveInlining for IsDecimalDigit
325ms Same as above + IsDecimalDigit call replaced with an inline check to avoid the redundant full width digit check
290ms Same as above + move the full width check at the end as such characters are probably rare
70ms Same as above + unsigned comparison + "partial inlining" (see below for details)

The last test assume that the "0" .. "9" is the most common case. In this case most of the code can be moved to a separate method and that allows the original method to be inlined:

    <MethodImpl(MethodImplOptions.AggressiveInlining)>
    Friend Function IntegralLiteralCharacterValue(Digit As Char) As Integer
        Dim value As Integer = AscW(Digit) - AscW("0"c)
        If (CType(value, UInt32) > CType(9, UInt32)) Then
            value = IntegralLiteralCharacterValueUncommon(Digit)
        End If
        Return value
    End Function

    Private Function IntegralLiteralCharacterValueUncommon(Digit As Char) As Integer
        Dim u As Integer = AscW(Digit)
        If Digit >= "A"c AndAlso Digit <= "F"c Then
            Return (u + (10 - AscW("A"c)))
        End If
        If Digit >= "a"c AndAlso Digit <= "f"c Then
            Return (u + (10 - AscW("a"c)))
        End If
        Debug.Assert(IsFullWidth(Digit), "Surprising digit")
        Return IntegralLiteralCharacterValue(MakeHalfWidth(Digit))
    End Function

The test time is a bit off in this case as ch is loop invariant and if inlining occurs so is the ch - '0' which the JIT compiler hoists out of the loop. Anyway, the difference between this version and the rest is significant enough for it to be an improvement even in the common case where ch isn't loop invariant.

How all this affects the scanner as a whole I do not know. This really should be tested by passing a significant amount of real world VB code through the scanner. That said, I picked IntegralLiteralCharacterValue because its input value distribution is pretty obvious unlike for, say, IsWhitespace. Anyway, the original code can be improved but using a dictionary isn't the way to go.

@gafter
Copy link
Member

gafter commented Nov 22, 2015

Thanks, @mikedn ! That is pretty compelling evidence that the Dictionary approach is worse than the current code by about a factor of three.

We won't accept this PR.

@gafter gafter closed this Nov 22, 2015
@gafter gafter added the Resolution-Won't Fix A real bug, but Triage feels that the issue is not impactful enough to spend time on. label Nov 22, 2015
@gafter gafter self-assigned this Nov 22, 2015
@AdamSpeight2008
Copy link
Contributor Author

@mikedn @gafter

Here's the results of my tests.(on my machine)

Test Code
Test Results

Method Total ms Avg ms
Dictionary 13360ms (Avg:= 318ms)
BinSearch 17751ms (Avg:= 422ms)
Mikedn's IntegralLiteralCharacterValue 4617ms (Avg:= 109ms)
Uncommon Dictionary 15842ms (Avg:= 377ms)
Split Dictionary 15038ms (Avg:= 358ms)
Select Case 2896ms (Avg:= 68ms)
Split Select Case 2976ms (Avg:= 70ms)

@AdamSpeight2008 AdamSpeight2008 deleted the Scanner(CharacterInfo) branch April 11, 2018 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla-already-signed Community The pull request was submitted by a contributor who is not a Microsoft employee. Resolution-Won't Fix A real bug, but Triage feels that the issue is not impactful enough to spend time on.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants