Resposta ao Desafio da Semana #3 [Performance - Comparando Expressões em VB.NET]

 

Por: Roberto Alexis Farah

Oi pessoal!

Eis a resposta do Desafio da Semana #3 https://blogs.technet.com/latam/archive/2006/05/05/427415.aspx .

O desafio ilustrou uma situação que pode ocorrer quando projetos são migrados de VB.NET para C# ou vice-versa.

PROBLEMA

O C# faz o que chamamos de curto-circuito ao resolver expressões lógicas. Ou seja a expressão pode não ser inteiramente analizada se parte da expressão for suficiente para se concluir o resultado.

Essa é uma herança do C/C++ e funciona assim:

AND OR XOR

1 0 = 0 1 0 = 1 1 0 = 1

0 1 = 0 0 1 = 1 0 1 = 1

1 1 = 1 1 1 = 1 1 1 = 0

0 0 = 0 0 0 = 0 0 0 = 0

Portanto em C# (ou C/C++) em situações como no exemplo abaixo resolver a primeira expressão é suficiente para se chegar a correta conclusão lógica. Se a primeira expressão (10 == numA) for falsa (False, 0) não é necessário se analisar a segunda expressão pois, independente do resultado da segunda expressão, a expressão toda será falsa, de acordo com a tabela acima.

If((10 == numA) && (20 == numB))

{

        // Executa…

}

O mesmo ocorre no exemplo abaixo. Se a primeira expressão for verdadeira a segunda não é analizada pois não vai influenciar no resultado final:

 

If((10 == numA) || (20 == numB))

{

        // Executa...

}

Pois bem, VB.NET por default mantém compatibilidade com VB 6.0 portanto, não faz esse tipo de análise otimizada de expressões booleanas. Entretanto, há operadores especiais do VB.NET usados justamente para isso.

Há um artigo que explica isso:

Description of "short-circuit" evaluation in Visual Basic

https://support.microsoft.com/kb/817250/en-us#EKACAAA

Portanto, VB.NET age como Visual Basic 6 quando usando os operadores herdados do Visual Basic 6, logo, a expressão é sempre completamente analizada.

E é esse comportamento que faz com que o resultado das expressões seja diferente no desafio proposto.

Entretanto, o VB.NET tem operadores especiais que fazem a análise da expressão do mesmo modo que o C#, conforme explicado no artigo acima.

Usando a ferramenta ILDASM que vem com o Visual Studio é possível ver o código Intermediate Language que é onde deveriam haver diferenças.

Código VB.NET original:

.method /*06000012*/ public static void Main() cil managed

// SIG: 00 00 01

{

  .entrypoint

  .custom /*0C000045:0A00001F*/ instance void [mscorlib/*23000001*/]System.STAThreadAttribute/*0100001A*/::.ctor() /* 0A00001F */ = ( 01 00 00 00 )

  // Method begins at RVA 0x22d4

  // Code size 138 (0x8a)

  .maxstack 3

  .locals /*1100000C*/ init ([0] bool VB$CG$t_bool$S0)

  .language '{3A12D0B8-C26C-11D0-B442-00A0244A1DD2}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'

// Source File 'C:\Development\My Tools\BLOG Articles\Article #7\CSharp\VB\Module1.vb'

  .line 5,5 : 5,15 'C:\\Development\\My Tools\\BLOG Articles\\Article #7\\CSharp\\VB\\Module1.vb'

//000005: Sub Main()

  IL_0000: /* 00 | */ nop

  .line 6,6 : 3,49 ''

//000006: If (DoSubtraction() > 0) Or (DoSum() > 0) Then

  IL_0001: /* 28 | (06)000013 */ call int32 VB.Module1/*02000007*/::DoSubtraction() /* 06000013 */

  IL_0006: /* 16 | */ ldc.i4.0

  IL_0007: /* FE02 | */ cgt

  IL_0009: /* 28 | (06)000014 */ call int32 VB.Module1/*02000007*/::DoSum() /* 06000014 */

  IL_000e: /* 16 | */ ldc.i4.0

  IL_000f: /* FE02 | */ cgt

  IL_0011: /* 60 | */ or

  IL_0012: /* 0A | */ stloc.0

  IL_0013: /* 06 | */ ldloc.0

  IL_0014: /* 2C | 1B */ brfalse.s IL_0031

  .line 7,7 : 7,82 ''

//000007: Console.WriteLine("Valor da soma das variaveis = {0:d}", sum + subtraction)

  IL_0016: /* 72 | (70)000001 */ ldstr "Valor da soma das variaveis = {0:d}" /* 70000001 */

  IL_001b: /* 7E | (04)000006 */ ldsfld int32 VB.Module1/*02000007*/::sum /* 04000006 */

  IL_0020: /* 7E | (04)000007 */ ldsfld int32 VB.Module1/*02000007*/::subtraction /* 04000007 */

  IL_0025: /* D6 | */ add.ovf

  IL_0026: /* 8C | (01)000018 */ box [mscorlib/*23000001*/]System.Int32/*01000018*/

  IL_002b: /* 28 | (0A)00001E */ call void [mscorlib/*23000001*/]System.Console/*01000019*/::WriteLine(string,

                                      object) /* 0A00001E */

  IL_0030: /* 00 | */ nop

  .line 8,8 : 3,9 ''

//000008: End If

  IL_0031: /* 00 | */ nop

  .line 10,10 : 3,11 ''

//000009:

//000010: sum = -5

  IL_0032: /* 1F | FB */ ldc.i4.s -5

  IL_0034: /* 80 | (04)000006 */ stsfld int32 VB.Module1/*02000007*/::sum /* 04000006 */

  .line 12,12 : 3,50 ''

//000011:

//000012: If (DoSubtraction() < 0) And (DoSum() < 0) Then

  IL_0039: /* 28 | (06)000013 */ call int32 VB.Module1/*02000007*/::DoSubtraction() /* 06000013 */

  IL_003e: /* 16 | */ ldc.i4.0

  IL_003f: /* FE04 | */ clt

  IL_0041: /* 28 | (06)000014 */ call int32 VB.Module1/*02000007*/::DoSum() /* 06000014 */

  IL_0046: /* 16 | */ ldc.i4.0

  IL_0047: /* FE04 | */ clt

  IL_0049: /* 5F | */ and

  IL_004a: /* 0A | */ stloc.0

  IL_004b: /* 06 | */ ldloc.0

  IL_004c: /* 2C | 1D */ brfalse.s IL_006b

  .line 13,13 : 5,80 ''

//000013: Console.WriteLine("Valor da soma das variaveis = {0:d}", sum + subtraction)

  IL_004e: /* 72 | (70)000001 */ ldstr "Valor da soma das variaveis = {0:d}" /* 70000001 */

  IL_0053: /* 7E | (04)000006 */ ldsfld int32 VB.Module1/*02000007*/::sum /* 04000006 */

  IL_0058: /* 7E | (04)000007 */ ldsfld int32 VB.Module1/*02000007*/::subtraction /* 04000007 */

  IL_005d: /* D6 | */ add.ovf

  IL_005e: /* 8C | (01)000018 */ box [mscorlib/*23000001*/]System.Int32/*01000018*/

  IL_0063: /* 28 | (0A)00001E */ call void [mscorlib/*23000001*/]System.Console/*01000019*/::WriteLine(string,

                                                                                                                      object) /* 0A00001E */

  IL_0068: /* 00 | */ nop

  IL_0069: /* 2B | 1C */ br.s IL_0087

  .line 14,14 : 3,7 ''

//000014: Else

  IL_006b: /* 00 | */ nop

  .line 15,15 : 4,84 ''

//000015: Console.WriteLine("Valor da subtracao das variaveis = {0:d}", sum - subtraction)

  IL_006c: /* 72 | (70)000049 */ ldstr "Valor da subtracao das variaveis = {0:d}" /* 70000049 */

  IL_0071: /* 7E | (04)000006 */ ldsfld int32 VB.Module1/*02000007*/::sum /* 04000006 */

  IL_0076: /* 7E | (04)000007 */ ldsfld int32 VB.Module1/*02000007*/::subtraction /* 04000007 */

  IL_007b: /* DA | */ sub.ovf

  IL_007c: /* 8C | (01)000018 */ box [mscorlib/*23000001*/]System.Int32/*01000018*/

  IL_0081: /* 28 | (0A)00001E */ call void [mscorlib/*23000001*/]System.Console/*01000019*/::WriteLine(string,

                                                                                                                      object) /* 0A00001E */

  IL_0086: /* 00 | */ nop

  .line 16,16 : 3,9 ''

//000016: End If

  IL_0087: /* 00 | */ nop

  .line 18,18 : 5,12 ''

//000017:

//000018: End Sub

  IL_0088: /* 00 | */ nop

  IL_0089: /* 2A | */ ret

} // end of method Module1::Main

Código C# :

.method /*06000001*/ private hidebysig static

        void Main(string[] args) cil managed

// SIG: 00 01 01 1D 0E

{

  .entrypoint

  // Method begins at RVA 0x2050

  // Code size 119 (0x77)

  .maxstack 3

  .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'

// Source File 'C:\Development\My Tools\BLOG Articles\Article #7\CSharp\CSharp\Program.cs'

  .line 15,15 : 12,54 'C:\\Development\\My Tools\\BLOG Articles\\Article #7\\CSharp\\CSharp\\Program.cs'

//000015: if((DoSubtraction() > 0) || (DoSum() > 0))

  IL_0000: /* 28 | (06)000002 */ call int32 CSharp.Program/*02000002*/::DoSubtraction() /* 06000002 */

  IL_0005: /* 16 | */ ldc.i4.0

  IL_0006: /* 30 | 08 */ bgt.s IL_0010

  IL_0008: /* 28 | (06)000003 */ call int32 CSharp.Program/*02000002*/::DoSum() /* 06000003 */

  IL_000d: /* 16 | */ ldc.i4.0

  IL_000e: /* 31 | 1A */ ble.s IL_002a

  .line 17,17 : 7,85 ''

//000016: {

//000017: Console.WriteLine("Valor da soma das variaveis = {0:d}\n", sum + subtraction);

  IL_0010: /* 72 | (70)000001 */ ldstr "Valor da soma das variaveis = {0:d}\n" /* 70000001 */

  IL_0015: /* 7E | (04)000001 */ ldsfld int32 CSharp.Program/*02000002*/::sum /* 04000001 */

  IL_001a: /* 7E | (04)000002 */ ldsfld int32 CSharp.Program/*02000002*/::subtraction /* 04000002 */

  IL_001f: /* 58 | */ add

  IL_0020: /* 8C | (01)000012 */ box [mscorlib/*23000001*/]System.Int32/*01000012*/

  IL_0025: /* 28 | (0A)000010 */ call void [mscorlib/*23000001*/]System.Console/*01000013*/::WriteLine(string,

                                                                                                                      object) /* 0A000010 */

  .line 20,20 : 10,19 ''

//000018: }

//000019:

//000020: sum = -5;

  IL_002a: /* 1F | FB */ ldc.i4.s -5

  IL_002c: /* 80 | (04)000001 */ stsfld int32 CSharp.Program/*02000002*/::sum /* 04000001 */

  .line 22,22 : 11,53 ''

//000021:

//000022: if((DoSubtraction() < 0) && (DoSum() < 0))

  IL_0031: /* 28 | (06)000002 */ call int32 CSharp.Program/*02000002*/::DoSubtraction() /* 06000002 */

  IL_0036: /* 16 | */ ldc.i4.0

  IL_0037: /* 2F | 23 */ bge.s IL_005c

  IL_0039: /* 28 | (06)000003 */ call int32 CSharp.Program/*02000002*/::DoSum() /* 06000003 */

  IL_003e: /* 16 | */ ldc.i4.0

  IL_003f: /* 2F | 1B */ bge.s IL_005c

  .line 24,24 : 6,84 ''

//000023: {

//000024: Console.WriteLine("Valor da soma das variaveis = {0:d}\n", sum + subtraction);

  IL_0041: /* 72 | (70)000001 */ ldstr "Valor da soma das variaveis = {0:d}\n" /* 70000001 */

  IL_0046: /* 7E | (04)000001 */ ldsfld int32 CSharp.Program/*02000002*/::sum /* 04000001 */

  IL_004b: /* 7E | (04)000002 */ ldsfld int32 CSharp.Program/*02000002*/::subtraction /* 04000002 */

  IL_0050: /* 58 | */ add

  IL_0051: /* 8C | (01)000012 */ box [mscorlib/*23000001*/]System.Int32/*01000012*/

  IL_0056: /* 28 | (0A)000010 */ call void [mscorlib/*23000001*/]System.Console/*01000013*/::WriteLine(string,

                                                                                                                      object) /* 0A000010 */

  IL_005b: /* 2A | */ ret

  .line 28,28 : 5,88 ''

//000025: }

//000026: else

//000027: {

//000028: Console.WriteLine("Valor da subtracao das variaveis = {0:d}\n", sum - subtraction);

  IL_005c: /* 72 | (70)00004B */ ldstr "Valor da subtracao das variaveis = {0:d}\n" /* 7000004B */

  IL_0061: /* 7E | (04)000001 */ ldsfld int32 CSharp.Program/*02000002*/::sum /* 04000001 */

  IL_0066: /* 7E | (04)000002 */ ldsfld int32 CSharp.Program/*02000002*/::subtraction /* 04000002 */

  IL_006b: /* 59 | */ sub

  IL_006c: /* 8C | (01)000012 */ box [mscorlib/*23000001*/]System.Int32/*01000012*/

  IL_0071: /* 28 | (0A)000010 */ call void [mscorlib/*23000001*/]System.Console/*01000013*/::WriteLine(string,

                                                                                                                      object) /* 0A000010 */

  .line 31,31 : 9,10 ''

//000029: }

//000030:

//000031: }

  IL_0076: /* 2A | */ ret

} // end of method Program::Main

SOLUÇÃO

Module Module1

            Dim sum As Integer = 50

            Dim subtraction As Integer = 100

            Sub Main()

                        If (DoSubtraction() > 0) OrElse (DoSum() > 0) Then

                                    Console.WriteLine("Valor da soma das variaveis = {0:d}", sum + subtraction)

                        End If

                        sum = -5

                        If (DoSubtraction() < 0) AndAlso (DoSum() < 0) Then

                        Console.WriteLine("Valor da soma das variaveis = {0:d}", sum + subtraction)

                        Else

                        Console.WriteLine("Valor da subtracao das variaveis = {0:d}", sum - subtraction)

                        End If

            End Sub

            Function DoSubtraction() As Integer

                        subtraction -= sum

                        DoSubtraction = subtraction

                        Exit Function

            End Function

            Function DoSum() As Integer

                        sum += subtraction

                        DoSum = sum

                        Exit Function

            End Function

End Module

Intermediate Language do código VB.NET usando os operadores que resolvem a expressão como em C# :

.method /*06000012*/ public static void Main() cil managed

// SIG: 00 00 01

{

  .entrypoint

  .custom /*0C000045:0A00001F*/ instance void [mscorlib/*23000001*/]System.STAThreadAttribute/*0100001A*/::.ctor() /* 0A00001F */ = ( 01 00 00 00 )

  // Method begins at RVA 0x22d4

  // Code size 144 (0x90)

  .maxstack 3

  .locals /*1100000C*/ init ([0] bool VB$CG$t_bool$S0)

  .language '{3A12D0B8-C26C-11D0-B442-00A0244A1DD2}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'

// Source File 'C:\Development\My Tools\BLOG Articles\Article #7\CSharp\Solution\Module1.vb'

  .line 5,5 : 2,12 'C:\\Development\\My Tools\\BLOG Articles\\Article #7\\CSharp\\Solution\\Module1.vb'

//000005: Sub Main()

  IL_0000: /* 00 | */ nop

  .line 6,6 : 3,53 ''

//000006: If (DoSubtraction() > 0) OrElse (DoSum() > 0) Then

  IL_0001: /* 28 | (06)000013 */ call int32 Solution.Module1/*02000007*/::DoSubtraction() /* 06000013 */

  IL_0006: /* 16 | */ ldc.i4.0

  IL_0007: /* 30 | 0B */ bgt.s IL_0014

  IL_0009: /* 28 | (06)000014 */ call int32 Solution.Module1/*02000007*/::DoSum() /* 06000014 */

  IL_000e: /* 16 | */ ldc.i4.0

  IL_000f: /* 30 | 03 */ bgt.s IL_0014

  IL_0011: /* 16 | */ ldc.i4.0

  IL_0012: /* 2B | 01 */ br.s IL_0015

  IL_0014: /* 17 | */ ldc.i4.1

  IL_0015: /* 0A | */ stloc.0

  IL_0016: /* 06 | */ ldloc.0

  IL_0017: /* 2C | 1B */ brfalse.s IL_0034

  .line 7,7 : 7,82 ''

//000007: Console.WriteLine("Valor da soma das variaveis = {0:d}", sum + subtraction)

  IL_0019: /* 72 | (70)000001 */ ldstr "Valor da soma das variaveis = {0:d}" /* 70000001 */

  IL_001e: /* 7E | (04)000006 */ ldsfld int32 Solution.Module1/*02000007*/::sum /* 04000006 */

  IL_0023: /* 7E | (04)000007 */ ldsfld int32 Solution.Module1/*02000007*/::subtraction /* 04000007 */

  IL_0028: /* D6 | */ add.ovf

  IL_0029: /* 8C | (01)000018 */ box [mscorlib/*23000001*/]System.Int32/*01000018*/

  IL_002e: /* 28 | (0A)00001E */ call void [mscorlib/*23000001*/]System.Console/*01000019*/::WriteLine(string,

                                                                                object) /* 0A00001E */

  IL_0033: /* 00 | */ nop

  .line 8,8 : 3,9 ''

//000008: End If

  IL_0034: /* 00 | */ nop

  .line 10,10 : 3,11 ''

//000009:

//000010: sum = -5

  IL_0035: /* 1F | FB */ ldc.i4.s -5

  IL_0037: /* 80 | (04)000006 */ stsfld int32 Solution.Module1/*02000007*/::sum /* 04000006 */

  .line 12,12 : 3,54 ''

//000011:

//000012: If (DoSubtraction() < 0) AndAlso (DoSum() < 0) Then

  IL_003c: /* 28 | (06)000013 */ call int32 Solution.Module1/*02000007*/::DoSubtraction() /* 06000013 */

  IL_0041: /* 16 | */ ldc.i4.0

  IL_0042: /* 2F | 08 */ bge.s IL_004c

  IL_0044: /* 28 | (06)000014 */ call int32 Solution.Module1/*02000007*/::DoSum() /* 06000014 */

  IL_0049: /* 16 | */ ldc.i4.0

  IL_004a: /* 32 | 03 */ blt.s IL_004f

  IL_004c: /* 16 | */ ldc.i4.0

  IL_004d: /* 2B | 01 */ br.s IL_0050

  IL_004f: /* 17 | */ ldc.i4.1

  IL_0050: /* 0A | */ stloc.0

  IL_0051: /* 06 | */ ldloc.0

  IL_0052: /* 2C | 1D */ brfalse.s IL_0071

  .line 13,13 : 5,80 ''

//000013: Console.WriteLine("Valor da soma das variaveis = {0:d}", sum + subtraction)

  IL_0054: /* 72 | (70)000001 */ ldstr "Valor da soma das variaveis = {0:d}" /* 70000001 */

  IL_0059: /* 7E | (04)000006 */ ldsfld int32 Solution.Module1/*02000007*/::sum /* 04000006 */

  IL_005e: /* 7E | (04)000007 */ ldsfld int32 Solution.Module1/*02000007*/::subtraction /* 04000007 */

  IL_0063: /* D6 | */ add.ovf

  IL_0064: /* 8C | (01)000018 */ box [mscorlib/*23000001*/]System.Int32/*01000018*/

  IL_0069: /* 28 | (0A)00001E */ call void [mscorlib/*23000001*/]System.Console/*01000019*/::WriteLine(string,

                                    object) /* 0A00001E */

  IL_006e: /* 00 | */ nop

  IL_006f: /* 2B | 1C */ br.s IL_008d

  .line 14,14 : 3,7 ''

//000014: Else

  IL_0071: /* 00 | */ nop

  .line 15,15 : 4,84 ''

//000015: Console.WriteLine("Valor da subtracao das variaveis = {0:d}", sum - subtraction)

  IL_0072: /* 72 | (70)000049 */ ldstr "Valor da subtracao das variaveis = {0:d}" /* 70000049 */

  IL_0077: /* 7E | (04)000006 */ ldsfld int32 Solution.Module1/*02000007*/::sum /* 04000006 */

  IL_007c: /* 7E | (04)000007 */ ldsfld int32 Solution.Module1/*02000007*/::subtraction /* 04000007 */

  IL_0081: /* DA | */ sub.ovf

  IL_0082: /* 8C | (01)000018 */ box [mscorlib/*23000001*/]System.Int32/*01000018*/

  IL_0087: /* 28 | (0A)00001E */ call void [mscorlib/*23000001*/]System.Console/*01000019*/::WriteLine(string,

                                                                                                                      object) /* 0A00001E */

  IL_008c: /* 00 | */ nop

  .line 16,16 : 3,9 ''

//000016: End If

  IL_008d: /* 00 | */ nop

  .line 18,18 : 2,9 ''

//000017:

//000018: End Sub

  IL_008e: /* 00 | */ nop

  IL_008f: /* 2A | */ ret

} // end of method Module1::Main

Comparando o código IL da solução em VB.NET ou do código C# contra o código original VB.NET notamos, em vermelho acima, os seguintes mnemônicos:

OR VB.NET original C# Solução em VB.NET

              cgt bgt bgt

              or

AND VB.NET original C# Solução em VB.NET

              clt bge bge

        and

A principal diferença é que o código C# e a solução em VB.NET usam instruções de branch, ou seja, instruções IL que mudam o fluxo de execução de acordo com uma condição. Os comandos de branch são identificados pela letra b inicial. No caso temos:

bgt = branch if greater than

bge = branch if greater or equal

Ainda olhando o IL notamos que há desvios baseados em condições que fazem com que a segunda comparação dos if() possa ou não ser executada.

E olhando o código VB.NET da solução original, tanto no fragmento usando OR quanto no fragmento usando AND notamos que não há desvios e que ambas condições de cada comparação são sempre executadas!

Note que o comando clt e cgt iniciam com c, são comandos de check, ou seja, eles não desviam o fluxo de execução como um branch apenas colocam a condição da checagem na pilha.

Se você quiser entender o código disassemblado em IL, eis uma boa referência:

https://msdn2.microsoft.com/en-us/library/system.reflection.emit(VS.80).aspx

Embora esse seja um problema simples com uma solução conhecida e documentada, ter conhecimento dessa particularidade dos operadores pode salvá-lo de bugs difíceis de serem detectados.

Além disso, AndAlso e OrElse geram um código mais otimizado, portanto, é preferível usar eles ao invés dos correspondentes And e Or em VB.NET.