Pergunta sobre compiler-construction, struct, c# – Método Value Type Equals - por que o compilador usa a reflexão?

15

Acabei de encontrar algo muito estranho para mim: quando você usa o método Equals () em um tipo de valor (e se esse método não foi substituído, é claro), você obtém algomuito muito lento - os campos são comparados um a um usando a reflexão! Como em :

<code>public struct MyStruct{
   int i;
}

   (...)

   MyStruct s, t;
   s.i = 0;
   t.i = 1;
   if ( s.Equals( t ))   /*  s.i will be compared to t.i via reflection here. */
      (...)
</code>

Minha pergunta: por que o compilador C # não gera um método simples para comparar tipos de valor? Algo como (na definição do MyStruct):

<code>   public override bool Equals( Object o ){
      if ( this.i == o.i )
         return true;
      else
         return false;
   }
</code>

O compilador sabe quais são os campos do MyStruct em tempo de compilação, por que ele espera até o tempo de execução para enumerar os campos MyStruct?

Muito estranho para mim.

Obrigado :)

ADICIONADO : Desculpe, acabei de perceber que, claro,Equals não é uma palavra-chave de linguagem, mas um método de tempo de execução ... O compilador é completamente inconsciente desse método. Então faça sentido aqui usar a reflexão.

"Para usar a implementação padrão de Equals, seu tipo de valor deve ser encaixotado e passado como uma instância do tipo de referência System.ValueType. O método Equals então usa a reflexão para realizar a comparação." - msdn.microsoft.com/pt-pt/library/ff647790.aspx MrPhil

Sua resposta

3   a resposta
3

Pensando nos efeitos, no entanto, acho que a equipe de design de idiomas fez o certo. Métodos compilados conhecidos do C ++ são difíceis de entender para iniciantes. Vamos ver o que aconteceria em C # com struct autogerado.Equals:

Como é agora, o conceito de .Equals () é simples:

Cada struct herda Equals de ValueType.Se substituído, o método Equals personalizado será aplicado.

Se o compilador sempre criasse o método Equals, poderíamos ter:

Cada struct herda Equals from Object. (ValueType não implementaria sua própria versão)Object.Equals agora é sempre (!) Substituído pelo método Equals gerado pelo compilador ou pela implementação dos usuários

Agora nossa estrutura tem um método de substituição autogerado que o leitor de código não vê! Então, como você sabe que o método base Object.Equals não se aplica à sua estrutura? Aprendendo todos os casos de métodos gerados automaticamente pelo compilador. E esta é exatamente uma das cargas que aprendem C ++.

Consideraria uma boa decisão deixar um struct eficiente igual ao usuário e manter os conceitos simples, exigindo um método padrão Equals padrão.

Dito isso, as estruturas críticas de desempenho devem substituir Equals. O código abaixo mostra

3606 vs53 milissegundos medido em .Net 4.5.1

Esse ganho de desempenho é certamente devido a evitar Equals virtuais, mas de qualquer maneira, portanto, se o Object.Equals virtual fosse chamado, o ganho seria muito menor. No entanto, os casos críticos de desempenho não chamarão Object.Equals, portanto, o ganho aqui será aplicado.

using System;
using System.Diagnostics;

struct A
{
    public int X;
    public int Y;
}

struct B : IEquatable<B>
{
    public bool Equals(B other)
    {
        return this.X == other.X && this.Y == other.Y;
    }

    public override bool Equals(object obj)
    {
        return obj is B && Equals((B)obj);
    }

    public int X;
    public int Y;
}


class Program
{
    static void Main(string[] args)
    {
        var N = 100000000;

        A a = new A();
        a.X = 73;
        a.Y = 42;
        A aa = new A();
        a.X = 173;
        a.Y = 142;

        var sw = Stopwatch.StartNew();
        for (int i = 0; i < N; i++)
        {
            if (a.Equals(aa))
            {
                Console.WriteLine("never ever");
            }
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);

        B b = new B();
        b.X = 73;
        b.Y = 42;
        B bb = new B();
        b.X = 173;
        b.Y = 142;

        sw = Stopwatch.StartNew();
        for (int i = 0; i < N; i++)
        {
            if (b.Equals(bb))
            {
                Console.WriteLine("never ever");
            }
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);
    }
}

Veja tambémhttp://blog.martindoms.com/2011/01/03/c-tip-override-equals-on-value-types-for-better-performance/

Nitpick: oReferenceEquals(null, obj) chamada é tecnicamente redundante comoais expressão é avaliada como falsa se a expressão fornecida (obj) é nulo. Tenho certeza de que isso não afeta os resultados do benchmark de qualquer maneira útil. Ainda assim, eu não confiaria no compilador para otimizá-lo se isso fosse importante. tne
... permitiu que o tempo de execução gerasse um comparador para cada tipo apenas na primeira vez em que é usado e acessasse esse comparador diretamente em chamadas subseqüentes. supercat
@tne: tão certo! removido. citykid
Vale a pena notar que o compilador não usa o Reflection; ele simplesmente usa o método de envio virtual para um métodoValueType.Equals; porque esse método esperathis ser um tipo de turma [ValueType é, apesar de seu nome, uma classe] o valor precisa ser encaixotado. Conceitualmente, poderia ter sido bom seValueType tinha definido um método estáticoValueTypeEquals<T>(ref T it, Object other) { ValueTypeComparer<T>.Compare(ref it, other); e recomendou que, quando possível, os compiladores chamem isso de preferência ao virtualEquals método. Tal abordagem poderia ter ... supercat
10

quando não precisa. Basta comparar os valores pouco a pouco, caso ostruct se puder fazer isso. No entanto, se algum dosstruct membros (ou membros de membros, quaisquer descendentes) substituirobject.Equals e fornecer sua própria implementação, obviamente, não pode confiar na comparação bit a bit para calcular o valor de retorno.

A razão é lenta é que o parâmetro paraEquals é do tipoobject e tipos de valor devem ser encaixados para serem tratados como umobject. O boxe envolve a alocação de memória no heap e na memória copiando o tipo de valor para esse local.

Você poderia fornecer manualmente uma sobrecarga para oEquals método que leva o seu própriostruct como parâmetro para evitar o boxe:

public bool Equals(MyStruct obj) {
     return obj.i == i;
}
Ele usa reflexão em alguns casos. Se detectar que pode apenas blit os resultados, ele faz isso - mas se houver tipos de referência (ou tipos contendo tipos de referência) nos campos, ele terá que fazer um processo mais doloroso. Jon Skeet
Eu tenho que dizer, eu não entendo porque uma comparação bit a bit não pode ser feita quando há referências. Se duas referências apontam para o mesmo objeto, quando os ponteiros não seriam exatamente iguais? snarf
Snarfblam - Eu ainda estou me perguntando sobre a mesma coisa. Sylvain Rodrigue
@ Snarfblam: A partir do código que Mehrdad postou, parece que ele chama Equals em cada um dos objetos referenciados contidos dentro da struct - quase recursivamente. Se dois objetos são iguais, mas suas referências não são, uma comparação bit a bit falhará. Samir Talwar
10

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    RuntimeType type = (RuntimeType) base.GetType();
    RuntimeType type2 = (RuntimeType) obj.GetType();
    if (type2 != type)
    {
        return false;
    }
    object a = this;
    if (CanCompareBits(this))
    {
        return FastEqualsCheck(a, obj);
    }
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    for (int i = 0; i < fields.Length; i++)
    {
        object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(a, false);
        object obj4 = ((RtFieldInfo) fields[i]).InternalGetValue(obj, false);
        if (obj3 == null)
        {
            if (obj4 != null)
            {
                return false;
            }
        }
        else if (!obj3.Equals(obj4))
        {
            return false;
        }
    }
    return true;
}

Quando possível, uma comparação baseada em bits será feita (observe o CanCompareBits e o FastEqualsCheck, ambos definidos como InternalCall. O JIT presumivelmente injetaria o código apropriado aqui. Quanto ao porquê é tão lento, eu não poderia lhe dizer .

Apenas teste isso. Você está certo. :) Sylvain Rodrigue
Gostaria de saber se haveria algum problema de compatibilidade se o tempo de execução fosse gerar automaticamente umEquals substituir por qualquer estrutura que ainda não definiu uma:bool Equals(object other) { return StructComparer<thisType>.EqualsProc(ref this, other); }, OndeEqualsProc era um campo delegado estático dentro da classe estáticaStructComparer<thisType>? Tal abordagem evitaria ter que usar o Reflection toda vez que um objeto fosse comparado, e também poderia evitar um passo de boxe. supercat

Perguntas relacionadas