Unable to build expression trees that use simd operations due to SIMD intrinsics requiring pointers.
My goal was to try doing an Expression Tree compiled function and see how it behaved with the new .net core 3.0 intrinsics. Create functions like Transform, Reduce, etc and then given basic expressions like
using System;
using VM.Core;
using System.Buffers;
using System.Linq.Expressions;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace VM.Math
{
public static class VMExpression
{
private static Expression BuildLessThanForLoop(Expression index,Expression end,Expression step,Func<Expression,Expression> block)
{
LabelTarget endLoop = Expression.Label();
return Expression.Loop(Expression.Block(new Expression[]
{
Expression.IfThen(Expression.LessThan(index,end),Expression.Break(endLoop)),
block(index),
Expression.AddAssign(index,step)
}),endLoop);
}
private static Expression CalculateVectorLength(Expression arrayLength,ConstantExpression vectorLength)
{
return Expression.And(arrayLength,Expression.OnesComplement(Expression.Subtract(vectorLength,Expression.Constant(1))));
}
private static Expression LoadAlignedVector128Float(Expression vector,Expression ptr)
{
return Expression.Assign(vector,Expression.Call(typeof(Sse2).GetMethod("LoadAlignedVector128",new Type[] { typeof(float*) }),new Expression[] { ptr }));
}
private static Expression StoreAligned128Float(Expression vector,Expression ptr)
{
return Expression.Call(typeof(Sse2).GetMethod("StoreAligned",new Type[] { typeof(float*), typeof(Vector128<float>) }),new Expression[] { ptr, vector });
}
private static Expression Index(Expression array,Expression index)
{
return Expression.ArrayIndex(array,new Expression[] { index });
}
private static Expression Add128Float(Expression vx, Expression vy)
{
return Expression.Call(typeof(Sse2).GetMethod("Add",new Type[] { typeof(Vector128<float>), typeof(Vector128<float>) }), new Expression[] { vx, vy });
}
private static Expression CreateAlignedArray(Expression count)
{
return Expression.New(typeof(AlignedFloatArray).GetConstructor(new Type[]{ typeof(int) }), new Expression[] { count });
}
public static unsafe Func<AlignedFloatArray,AlignedFloatArray,AlignedFloatArray> Transform(Expression<Func<float,float,float>> expr)
{
var paramX = Expression.Parameter(typeof(AlignedFloatArray),"x");
var paramY = Expression.Parameter(typeof(AlignedFloatArray),"y");
var parameters = new ParameterExpression[] { paramX, paramY };
var result = Expression.Variable(typeof(AlignedFloatArray),"result");
var memX = Expression.Variable(typeof(Memory<float>),"memX");
var memY = Expression.Variable(typeof(Memory<float>),"memY");
var memZ = Expression.Variable(typeof(Memory<float>),"memZ");
var pinX = Expression.Variable(typeof(MemoryHandle),"pinX");
var pinY = Expression.Variable(typeof(MemoryHandle),"pinY");
var pinZ = Expression.Variable(typeof(MemoryHandle),"pinZ");
var ptrX = Expression.Variable(typeof(float*),"ptrX");
var ptrY = Expression.Variable(typeof(float*),"ptrY");
var ptrZ = Expression.Variable(typeof(float*),"ptrZ");
var i = Expression.Variable(typeof(int),"i");
var n = Expression.Variable(typeof(int),"n");
var step = Expression.Constant(Vector128<float>.Count);
var length = Expression.Variable(typeof(int),"length");
var vx = Expression.Variable(typeof(Vector128<float>));
var vy = Expression.Variable(typeof(Vector128<float>));
var returnLabel = Expression.Label();
var methodBlock = Expression.Block(parameters, new Expression[]
{
Expression.Assign(result,CreateAlignedArray(Expression.PropertyOrField(paramX,"Length"))),
Expression.Assign(memX,Expression.Call(paramX,typeof(AlignedFloatArray).GetMethod("AsMemory"))),
Expression.Assign(memY,Expression.Call(paramY,typeof(AlignedFloatArray).GetMethod("AsMemory"))),
Expression.Assign(memZ,Expression.Call(result,typeof(AlignedFloatArray).GetMethod("AsMemory"))),
Expression.Assign(pinX,Expression.Call(memX,typeof(Memory<float>).GetMethod("Pin"))),
Expression.Assign(pinY,Expression.Call(memY,typeof(Memory<float>).GetMethod("Pin"))),
Expression.Assign(pinZ,Expression.Call(memZ,typeof(Memory<float>).GetMethod("Pin"))),
Expression.Assign(ptrX,Expression.Convert(Expression.PropertyOrField(pinX,"Pointer"),typeof(float*))),
Expression.Assign(ptrY,Expression.Convert(Expression.PropertyOrField(pinY,"Pointer"),typeof(float*))),
Expression.Assign(ptrZ,Expression.Convert(Expression.PropertyOrField(pinZ,"Pointer"),typeof(float*))),
Expression.TryFinally(Expression.Block(new Expression[]
{
Expression.Assign(i,Expression.Constant(0)),
Expression.Assign(length,Expression.PropertyOrField(paramX,"Length")),
Expression.Assign(n,CalculateVectorLength(length,step)),
Expression.IfThen(Expression.GreaterThanOrEqual(n,step),Expression.Block(new Expression[]
{
BuildLessThanForLoop(i,n,step,(index) => {
return Expression.Block(new Expression[]
{
LoadAlignedVector128Float(vx,ptrX),
LoadAlignedVector128Float(vy,ptrY),
StoreAligned128Float(ptrZ,Add128Float(vx,vy))
});
})
})),
BuildLessThanForLoop(i,length,Expression.Constant(1),(index) =>
{
return Expression.Block(new Expression[]
{
Expression.Assign(Index(ptrZ,index),Expression.Add(Index(ptrX,index),Index(ptrY,index)))
});
}),
Expression.Return(returnLabel,result)
}),
Expression.Block(new Expression[]
{
Expression.Call(pinX,typeof(MemoryHandle).GetMethod("Dispose")),
Expression.Call(pinY,typeof(MemoryHandle).GetMethod("Dispose")),
Expression.Call(pinZ,typeof(MemoryHandle).GetMethod("Dispose"))
}))
});
return (Func<AlignedFloatArray,AlignedFloatArray,AlignedFloatArray>)Expression.Lambda(methodBlock,parameters).Compile(false);
}
}
}
At the moment there doesn't appear to be any reasonable way to do a load or store though using expression trees because they require pointer types. I get an exception if I try to declare a pointer variable in an expression tree.
Exception has occurred: CLR/System.ArgumentException
An unhandled exception of type 'System.ArgumentException' occurred in System.Linq.Expressions.dll: 'Type must not be a pointer type'
at System.Dynamic.Utils.TypeUtils.ValidateType(Type type, String paramName, Boolean allowByRef, Boolean allowPointer)
at System.Linq.Expressions.Expression.Validate(Type type, Boolean allowByRef)
at System.Linq.Expressions.Expression.Variable(Type type, String name)
at MLNet.Math.VMExpression.Transform(Expression`1 expr) in /home/mltrainer/Desktop/mlnet/math/VM-Expression.cs:line 67
at MLNet.Program.Main(String[] args) in /home/mltrainer/Desktop/mlnet/Program.cs:line 23
I also don't see any OpCodes to output through System.Reflection.Emit either that correspond to what would appear like the new Vector instructions.
Is there any way to compile an unsafe function using the new SIMD operations through any mechanism that is currently supported within .net core? Would make C# able to do some operations that would normally have a large number of temporaries very efficiently compared to other languages like python and even C++ where expression templates are used by libraries like Eigen.
Expression trees can use some love in general; there are many language constructs which are unrepresentable with them.
I also don't see any OpCodes to output through System.Reflection.Emit either that correspond to what would appear like the new Vector instructions.
Why would you need any special OpCodes for that? Have you tried calling the new methods?
This issue belongs in the csharplang repo, speaking of which: dotnet/csharplang#2545
I also don't see any OpCodes to output through System.Reflection.Emit either that correspond to what would appear like the new Vector instructions.
Correct, because there don't exist any IL opcodes for those. The Vector methods are implemented as JIT intrinsics so that it's possible to run them on CPUs that don't support SIMDs (by switching to a software fallback implementation).
Most helpful comment
Expression trees can use some love in general; there are many language constructs which are unrepresentable with them.