FluentIL – Parte 3 – DSL Improvements

Posted on fevereiro 19, 2011 by

0


Olá pessoal, tudo certo?

Este é mais um post da série que acompanha o desenvolvimento de uma DSL (antes chamada de “toolkit”) que facilita o desenvolvimento de código para emitting. Embora essa série trate de aspectos avançados de emitting e Intermediate Language, também percebo como uma excelente oportunidade de exercício para construção de DSLs.

No primeiro post, mostrei como um helper pode facilitar atividades verbosas e complexas como, nesse contexto especificamente, a utilização das bibliotecas de emitting do .net em atividades mais diretas e com mais significado. Para isso, apenas indiquei uma simplificação da sintaxe.

No segundo post, fui um pouco adiante. Indiquei como esse mesmo helper pode “ocultar” complexidade de uma API, propondo um paradigma diferente do utilizado originalmente. No post em questão, mostrei como substituir a lógica condicional do IL (If-GoTo) por uma lógica mais próxima de linguagens de alto nível (If..Else..Endif)

Alguns comentários recebidos para o segundo post deixaram evidente que nem todos que acompanham o blog conseguem perceber utilidade prática para emitting e IL. Isso foi um presente! Em função dessa condição, escrevi um post com um exemplo prático e útil para utilização dessas tecnologias. A escrita desse post me levou ao desenvolvimento de alguns recursos faltantes que foram implementados. 

Ao observar com cuidado o código desenvolvido usando a DSL no exemplo prático, percebi algumas oportunidades extras de melhoria. O post de hoje, trata dos recursos adicionados para escrita do exemplo prático e das melhorias adicionadas na sequência. Aliás, todo tempo investido na criação de demos para nossos “componentes” é tempo muito bem investido. Ser “usuário” de nossos códigos é oportunidade para identificarmos fragilidades de nossa implementação.

Todo o código fonte dessa série está disponível no GitHub (https://github.com/ElemarJR/FluentIL)

Nosso código de estudo

Nosso ponto de partida, para as melhorias em nossa DSL é o código escrito com ela no exemplo prático. Repito o código aqui:


              
static DynamicMethod CreateFilterMethodIL(byte[] src, byte[] dst,
    int stride, int bytesPerPixel, double[] filter, int filterWidth, int bias)
{
    #region filter analysis

    int filterHeight = filter.Length / filterWidth;

    bool allwaysFilterNeg = true;
    bool neverFilterNeg = true;
    double negs = 0;
    for (int i = 0; i < filter.Length; i++)
    {
        if (filter[i] > 0) allwaysFilterNeg = false;
        if (filter[i] < 0)
        {
            neverFilterNeg = false;
            negs += filter[i];
        }
    }

    #endregion

    if (filter[filter.Length / 2] >= Math.Abs(negs)) neverFilterNeg = true;

    var result = IL.NewMethod()
        .WithParameter(typeof(byte[]), "src")
        .WithParameter(typeof(byte[]), "dest")
        .WithVariable(typeof(double), "pixelsAccum")
        .WithVariable(typeof(double), "filterAccum")
        .Returns(typeof(void))

        .For("iDst", 0, src.Length - 1)
            .LdcR8(0.0)
            .Dup()
            .Stloc("pixelsAccum", "filterAccum")

            .Repeater(0, filter.Length - 1, 1,
                (ind, body) => filter[ind] != 0,
                (index, body) =>
                {
                    body
                        .Ldarg("src")
                        .Ldloc("iDst")
                        .Add(IlApplier.ComputeOffset(index, filterWidth, stride, bytesPerPixel))

                        .Dup()
                        .LdcI4(-1)
                        .Ifgt()
                            .Dup()
                            .LdcI4(src.Length)
                            .Iflt()
                                .LdelemU1()
                                .ConvR8()
                                .Mul(filter[index])

                                .Ldloc("pixelsAccum")
                                .Add()
                                .Stloc("pixelsAccum")

                                .Ldloc("filterAccum")
                                .Add(filter[index])
                                .Stloc("filterAccum")

                            .Else()
                                .Pop().Pop()
                            .EndIf()
                        .Else()
                            .Pop().Pop()
                        .EndIf();
                }
            )

            .Ldarg("dest")
            .Ldloc("iDst")

            .Ldloc("pixelsAccum", "filterAccum")

            .Dup()
            .LdcR8(0)
            .IfNoteq()
                .EmitIf(!neverFilterNeg && allwaysFilterNeg, (r) => r.Neg())
                .EmitIf(!neverFilterNeg && !allwaysFilterNeg, (r) => r
                    .Dup().LdcR8(0.0)
                    .Iflt()
                        .Neg()
                    .EndIf()
                )
                .Div()
            .Else()
                .Pop()
            .EndIf()

            .Add((double)bias)

            .EnsureLimits(0.0, 255.0)
            .ConvU1()
            .StelemI1()
        .Next()
        .Ret();

    return result;
}

            

Gostaria de evidenciar a preocupação que tive em criar um “código sem desvios”. Repare, nas linhas 81 e 82, a presença do método utilitário EmitIf. O objetivo dessa “intrução” é não interromper a “fluência” do código com “condicionais”. Segue sua implementação:


              
public DynamicMethodBody EmitIf(bool condition, Action action)
{
    if (condition)
        action(this);

    return this;
}

            

Utilizei recurso semelhante para impedir a interrupção da “fluência” para inserção de um for. Construi um Repeater (linha 37 do código “exercício”). Segue implementação:


              
public DynamicMethodBody Repeater(int from, int to, int step,
    Action action
    )
{
    for (int i = from; i <= to; i += step)
        action(i, this);

    return this;
}

public DynamicMethodBody Repeater(int from, int to, int step,
    Func precondition,
    Action action
    )
{
    for (int i = from; i <= to; i += step)
        if (precondition(i, this))
            action(i, this);

    return this;
}

            

Simples!

Outra melhoria que fiz, imediatamente, foi promover um grande bloco de IL para colocar o valor que estava na stack em determinados limites a instrução da DSL. Essa instrução é EnsureLimits. Observe sua implementação:


              
public DynamicMethodBody EnsureLimits(double min, double max)
{
    return this
        .Dup()
        .LdcR8(min)
        .Iflt()
            .Pop()
            .LdcR8(min)
        .Else()
            .Dup()
            .LdcR8(max)
            .Ifgt()
                .Pop()
                .LdcR8(max)
            .EndIf()
        .EndIf();
}

            

Implementar lógica freqüente como parte da DSL permite que o programador trabalhe com um nível de abstração mais alto.

Oportunidades de melhoria

Embora o código escrito utilizando nossa DSL tenha ficado bem mais simples de entender (e escrever) que o original (veja post para ver esse código) utilizando a biblioteca padrão do .net, ainda possui muitos pontos de melhoria.

Atribuição de variáveis “carrega-salva”

Atribuição de valores em variáveis na Intermediate Language, por padrão, implica na carga de valores para a pilha, seguida de armazenamento em uma variável. Sempre que o valor é atribuído, é retirado da pilha. No código de exemplo, isso ficou explício nesse bloco:


              
// ...
        .LdcR8(0.0)
        .Dup()
        .Stloc("pixelsAccum", "filterAccum")
//...

            

Natural, mas verboso.

Somar valor na pilha com valor de variável, armazenando resultado na variável

Somar um valor na pilha, com o valor de uma variável, armazenando resultado nessa variável implica em três instruções. Observe:


              
// ...
        .Ldloc("pixelsAccum")
        .Add()
        .Stloc("pixelsAccum")
// ...

            

Mais uma vez: Natural, mas verboso!

Somar valor constante com valor de variável, armazenando resultado na variável

Outra vez, três operações. Observe:


              
// ..
        .Ldloc("filterAccum")
        .Add(filter[index])
        .Stloc("filterAccum")
// ..

            

“Abs” disfarçado

Em dado momento foi necessário garantir que o valor presente na pilha fosse absoluto (positivo). Para isso, foi escrito o seguinte bloco de código:


              
// ..
        .Dup().LdcR8(0.0)
   .Iflt()
                .Neg()
   .EndIf()
// ..

            

É muito “código cliente” para executar uma operação tão comum.

Dificuldade de expor condicionais complexos

Nossa implementação “If..Else” inicial não previa condicionais com operadores lógicos. Isso resultou em código assim:


              
// ..
    .Dup()
    .LdcI4(-1)
    .Ifgt()
        .Dup()
        .LdcI4(src.Length)
        .Iflt()
            .LdelemU1()
            .ConvR8()
            .Mul(filter[index])

            .Ldloc("pixelsAccum")
            .Add()
            .Stloc("pixelsAccum")

            .Ldloc("filterAccum")
            .Add(filter[index])
            .Stloc("filterAccum")

        .Else()
            .Pop().Pop()
        .EndIf()
    .Else()
        .Pop().Pop()
    .EndIf()
//..

            

É muita coida para escrever algo como “if (x >= 0 && x <= src.length) …

Nosso código de estudo, com uma DSL melhorada

Verificando as diversas oportunidades de melhoria, foram feitas implementações para cada uma delas. O código de estudo, revisado e melhorado, ficou assim:


              
static DynamicMethod CreateFilterMethodIL(byte[] src, byte[] dst,
    int stride, int bytesPerPixel, double[] filter, int filterWidth, int bias)
{
    #region filter analysis
        // .. omitindo o código de analise do filtro por não ser relevante nesse contexto
    #endregion

    if (filter[filter.Length / 2] >= Math.Abs(negs)) neverFilterNeg = true;

    var result = IL.NewMethod()
        .WithParameter(typeof(byte[]), "src")
        .WithParameter(typeof(byte[]), "dest")
        .WithVariable(typeof(double), "pixelsAccum")
        .WithVariable(typeof(double), "filterAccum")
        .Returns(typeof(void))

        .For("iDst", 0, src.Length - 1)
            .Stloc(0.0, "pixelsAccum", "filterAccum")

            .Repeater(0, filter.Length - 1, 1,
                (ind, body) => filter[ind] != 0,
                (index, body) =>
                {
                    body
                        .Ldarg("src")
                        .Ldloc("iDst")
                        .Add(IlApplier.ComputeOffset(index, filterWidth, stride, bytesPerPixel))
                        .Dup()
                        .Ifge(0).Andlt(src.Length)
                            .LdelemU1()
                            .ConvR8()
                            .Mul(filter[index])
                            .AddToVar("pixelsAccum")
                            .AddToVar("filterAccum", filter[index])
                        .Else()
                            .Pop().Pop()
                        .EndIf();
                }
            )

            .Ldarg("dest")
            .Ldloc("iDst", "pixelsAccum", "filterAccum")

            .Dup()
            .IfNoteq(0.0)
                .EmitIf(!neverFilterNeg && allwaysFilterNeg, (r) => r.Neg())
                .EmitIf(!neverFilterNeg && !allwaysFilterNeg, (r) => r.AbsR8())
                .Div()
            .Else()
                .Pop()
            .EndIf()

            .Add((double)bias)

            .EnsureLimits(0.0, 255.0)
            .ConvU1()
            .StelemI1()
        .Next()
        .Ret();

    return result;
}

            

As melhorias implementadas foram:

  • Criei uma sobrecarga para Stloc que aceita o valor a ser atribuído como primeiro parâmetro;
  • Em comparações com constantes, criei sobrecargas nos métodos If que recebem essas constantes;
  • Criei um método AddToVar que “carrega o valor de uma variável”, adiciona valor da pilha e armazena resultado na variável;
  • Criei uma sobrecarga para AddToVar que carrega um valor constante, o valor da variável, soma-os e armazena o resultado na variável;
  • Criei um método Abs, melhorando abstração do código;
  • Criei métodos And para permitir a criação de condições mais complexas (somente usando constantes).

Penso que o código ficou, além de menor, bem mais fácil de ler e entender. O que você acha?

Veja como implementei essas funcionalidades pegando o código fonte no Github: https://github.com/ElemarJR/FluentIL. Há muito T4, testes e truques bacanas de implementação.

Por hoje, era isso.