C
C#•3y ago
LPeter1997

Speed up reflection-based comparison

We have a type that we have no access to. Not now, never. And we can't replace it. We have a couple of private fields in it that we need to compare inside two instances to determine if they are equivalent for our purposes. Now the thing is, the place where the comparisons happen is super performance-critical. Is there a way to build up and "compile" a comparison function like this at runtime purely from reflection info to speed this up?
20 Replies
LPeter1997
LPeter1997OP•3y ago
It could be that what I need is expression trees? For reasons it would be nice not to use 3rd party here @Diddy didn't you send replies here
canton7
canton7•3y ago
Are you caching the FieldInfo etc? That's the first big win Compiled expressions will give you a little performance boost on top of that
LPeter1997
LPeter1997OP•3y ago
I guess the best course of action is to set up a test with BDN and see for myself Diddy acting... weird again No bueno Impossible Changes all the time and there are many-many different types (They are the async state machines the functions generate) Anyone interested, this is what I ended up with, performs ok: https://gist.github.com/LPeter1997/47886848e145fb61eb49c32344c03abf (Note: the code since has been updated to accommodate release mode, but I don't think anyone else would need this ever when )
canton7
canton7•3y ago
Nit: if you do Expression.Lambda<Func<...>>(...), you don't then need to cast the result of lambda.Compile()
LPeter1997
LPeter1997OP•3y ago
Oh I didn't know that, thanks
canton7
canton7•3y ago
What's the need for the Unsafe.As? Also, you call Unsafe.As once for each field, rather than once
LPeter1997
LPeter1997OP•3y ago
I don't want to cast the member each time to the concrete state machine type, because I know it's that type anyway, and this is running in critical code
canton7
canton7•3y ago
Ah, I see. I'd use Expression.Convert there
LPeter1997
LPeter1997OP•3y ago
So no need for the typecheck each time
canton7
canton7•3y ago
But even so, you can store the result of Unsafe.As in a variable
LPeter1997
LPeter1997OP•3y ago
I know but I also don't know how to factor it out to only have to cast it once. I haven't used the expr tree in years and IIRC they were sort of for single expressions. Like I don't recall being able to declare locals My knowledge is (likely) seriously outdated on this tho Sadly in practice, my code will likely write a convert anyway, because in release mode, Roslyn generates struct state machines, which means Unsafe.As is illegal anyway pepehands
canton7
canton7•3y ago
Expression.Block.
var castParam1 = Expression.Variable(...);
...
var blockMembers = new Expression[]
{
Expression.Assign(castParam1, unsafeAs),
Expression.Assign(castParam2, unsafeAs),
comparisonsConjuncted,
};
var block = Expression.Block(new[] { castParam1, castParam2 }, blockMembers);
var lambda = Expression.Lambda<...>(block, new[] { param1, param2 });
var castParam1 = Expression.Variable(...);
...
var blockMembers = new Expression[]
{
Expression.Assign(castParam1, unsafeAs),
Expression.Assign(castParam2, unsafeAs),
comparisonsConjuncted,
};
var block = Expression.Block(new[] { castParam1, castParam2 }, blockMembers);
var lambda = Expression.Lambda<...>(block, new[] { param1, param2 });
(and use castParam1 etc in comparisons of course) The last expression in a block is the return value. You need to give it the list of variables used in the block as well
LPeter1997
LPeter1997OP•3y ago
Does it implement them (locals) with closures or something
canton7
canton7•3y ago
No. It translates to a BasicBlock of instructions Just a set of things executed one after another System.Linq.Expressions aren't always pure expressions 😛 (IIRC this stuff was added on in C# 4 for dynamic support)
LPeter1997
LPeter1997OP•3y ago
I'll hammer my hash-code function into this form to test it first, that's simpler Yeah this doesn't work, I'm getting the exception I kind of expected I might be screwing something up tho
private static AsmGetHashCodeDelegate CreateHashCodeFunc(Type asmType)
{
var getTypeMethod = asmType.GetMethod(nameof(GetType))!;
var toHashCodeMethod = typeof(HashCode).GetMethod(nameof(HashCode.ToHashCode))!;

var param = Expression.Parameter(typeof(IAsyncStateMachine));

var hashCode = Expression.Variable(typeof(HashCode));
var asmAsConcreteType = Expression.Variable(asmType);

var blockExprs = new List<Expression>();
blockExprs.Add(Expression.Assign(hashCode, Expression.Default(typeof(HashCode))));
blockExprs.Add(Expression.Assign(asmAsConcreteType, CastToConcreteType(param, asmType)));

foreach (var field in GetRelevantFields(asmType))
{
blockExprs.Add(Expression.Call(
instance: hashCode,
methodName: nameof(HashCode.Add),
typeArguments: new[] { field.FieldType },
arguments: Expression.MakeMemberAccess(asmAsConcreteType, field)));
}

blockExprs.Add(Expression.Call(
instance: hashCode,
method: toHashCodeMethod));

var block = Expression.Block(blockExprs);
var lambda = Expression.Lambda<Func<IAsyncStateMachine, int>>(block, param);

return new(lambda.Compile());
}
private static AsmGetHashCodeDelegate CreateHashCodeFunc(Type asmType)
{
var getTypeMethod = asmType.GetMethod(nameof(GetType))!;
var toHashCodeMethod = typeof(HashCode).GetMethod(nameof(HashCode.ToHashCode))!;

var param = Expression.Parameter(typeof(IAsyncStateMachine));

var hashCode = Expression.Variable(typeof(HashCode));
var asmAsConcreteType = Expression.Variable(asmType);

var blockExprs = new List<Expression>();
blockExprs.Add(Expression.Assign(hashCode, Expression.Default(typeof(HashCode))));
blockExprs.Add(Expression.Assign(asmAsConcreteType, CastToConcreteType(param, asmType)));

foreach (var field in GetRelevantFields(asmType))
{
blockExprs.Add(Expression.Call(
instance: hashCode,
methodName: nameof(HashCode.Add),
typeArguments: new[] { field.FieldType },
arguments: Expression.MakeMemberAccess(asmAsConcreteType, field)));
}

blockExprs.Add(Expression.Call(
instance: hashCode,
method: toHashCodeMethod));

var block = Expression.Block(blockExprs);
var lambda = Expression.Lambda<Func<IAsyncStateMachine, int>>(block, param);

return new(lambda.Compile());
}
System.InvalidOperationException: 'variable '' of type 'System.HashCode' referenced from scope '', but it is not defined'
canton7
canton7•3y ago
You need to give it the list of variables used in the block as well
LPeter1997
LPeter1997OP•3y ago
Oh crap yes the block
canton7
canton7•3y ago
So Expression.Block(new[] { hashCode, asmAsConcreteType }, ...)
LPeter1997
LPeter1997OP•3y ago
Yea yea Ok, this works, thanks Weird design, since we are working with symbols effectively already
canton7
canton7•3y ago
It makes compilation easier I think?
Want results from more Discord servers?
Add your server