Worse performance with pointers?

I don't understand how the unsafe method performs slower than the safe version. Below is the custom struct and benchmarks.
[StructLayout(LayoutKind.Explicit, Size = 16)]
public readonly partial struct MyCustomStruct
{

[FieldOffset(0)]
private readonly ulong Ulong0;
[FieldOffset(8)]
private readonly ulong Ulong1;

private MyCustomStruct(in ulong ulong0, in ulong ulong1)
{
this.Ulong0 = ulong0;
this.Ulong1 = ulong1;
}

public unsafe MyCustomStruct SetBitUnsafe(in int bitPosition)
{
ulong mask = 1UL << (bitPosition % 64);

// copy to new val so we do not modify original
MyCustomStruct returnVal = this;

ulong* ptr = &(returnVal.Ulong0);

ptr[(bitPosition / 64)] |= mask;

return returnVal;
}

public MyCustomStruct SetBitSafe(in int bitPosition)
{
int ulongIndex = bitPosition / 64;
int bitIndex = bitPosition % 64;
ulong mask = (1UL << bitIndex);
ulong ulong0 = Ulong0;
ulong ulong1 = Ulong1;

switch (ulongIndex)
{
case 0:
ulong0 |= mask;
break;
case 1:
ulong1 |= mask;
break;
default:
throw new ArgumentOutOfRangeException(nameof(bitPosition), "Bit position is out of range.");
}

return new MyCustomStruct(in ulong0, in ulong1);
}
}


//Benchmarks
[Benchmark]
public MyCustomStruct SetBitSafe_Test()
{
MyCustomStruct returnVal = new();
for (int i = 0; i < 10000; i++)
{
returnVal = new MyCustomStruct().SetBitSafe(67);
}

return returnVal;
}

[Benchmark]
public MyCustomStruct SetBitUnsafe_Test()
{
MyCustomStruct returnVal = new();
for (int i = 0; i < 10000; i++)
{
returnVal = new MyCustomStruct().SetBitUnsafe(67);
}

return returnVal;
}
[StructLayout(LayoutKind.Explicit, Size = 16)]
public readonly partial struct MyCustomStruct
{

[FieldOffset(0)]
private readonly ulong Ulong0;
[FieldOffset(8)]
private readonly ulong Ulong1;

private MyCustomStruct(in ulong ulong0, in ulong ulong1)
{
this.Ulong0 = ulong0;
this.Ulong1 = ulong1;
}

public unsafe MyCustomStruct SetBitUnsafe(in int bitPosition)
{
ulong mask = 1UL << (bitPosition % 64);

// copy to new val so we do not modify original
MyCustomStruct returnVal = this;

ulong* ptr = &(returnVal.Ulong0);

ptr[(bitPosition / 64)] |= mask;

return returnVal;
}

public MyCustomStruct SetBitSafe(in int bitPosition)
{
int ulongIndex = bitPosition / 64;
int bitIndex = bitPosition % 64;
ulong mask = (1UL << bitIndex);
ulong ulong0 = Ulong0;
ulong ulong1 = Ulong1;

switch (ulongIndex)
{
case 0:
ulong0 |= mask;
break;
case 1:
ulong1 |= mask;
break;
default:
throw new ArgumentOutOfRangeException(nameof(bitPosition), "Bit position is out of range.");
}

return new MyCustomStruct(in ulong0, in ulong1);
}
}


//Benchmarks
[Benchmark]
public MyCustomStruct SetBitSafe_Test()
{
MyCustomStruct returnVal = new();
for (int i = 0; i < 10000; i++)
{
returnVal = new MyCustomStruct().SetBitSafe(67);
}

return returnVal;
}

[Benchmark]
public MyCustomStruct SetBitUnsafe_Test()
{
MyCustomStruct returnVal = new();
for (int i = 0; i < 10000; i++)
{
returnVal = new MyCustomStruct().SetBitUnsafe(67);
}

return returnVal;
}
No description
No description
4 Replies
Anton
Anton3mo ago
The safe case is probably easier for the compiler to understand Look at the optimized codegen
nathanAjacobs
nathanAjacobs3mo ago
How would I do that?
Anton
Anton3mo ago
sharplab it has an option to view assembly as well (machine code)
Angius
Angius3mo ago
The compiler is generally very good at optimizing the code. If you're trying to optimize it for it, ahead of time, chances are you'll do a worse job at it It's not C++, fuck pointers
Want results from more Discord servers?
Add your server