Skip to content

Commit

Permalink
Generate branchless IL for (b ? 1 : 0) (#67191)
Browse files Browse the repository at this point in the history
* Add tests

* Generate branchless IL for `(b ? 1 : 0)`

* Update IL snapshots

* Move BuildValidator to 64 bit

This process uses a significant amount of memory as it's essentially
doing compilation. Like the compiler server it needs to be 64 bit on 64
platforms to safely host all the items. Moved to explicit PlatformTarget
of AnyCPU so it's not silently run as x86 in Debug.

* Rename `isOne` out parameter

* Add nonbinary test

* Restrict the optimization

To only comparison operations.
Also always convert to bool.

* Test IL in Debug

* Encapsulate the emit into method

* Convert result

* Disable optimization in Debug

* Improve wording

* Check type

* Rename emit comparison method

* Clarify test options

* Extend tests

* Revert "Move BuildValidator to 64 bit"

This reverts commit 333d1aa.

* Optimize `char`s

* Remove unnecessary `char` check

* Extend `char` tests

* Add VB tests

* Emit branchless IL for `If(b, 1, 0)` in VB

* Update IL snapshot

* Add sequence points to IL snapshots

* Test `isinst`

* Optimize `isinst`

* Optimize `is` codegen when used inside condition

* Rename parameter which omits boolean conversion of `is` codegen

* Clarify return values

* Improve variable naming

* Test IntPtr in VB

* Test `ILEmitStyle.DebugFriendlyRelease`

---------

Co-authored-by: Jared Parsons <jaredpparsons@gmail.com>
  • Loading branch information
jjonescz and jaredpar committed Apr 17, 2023
1 parent 9f323e7 commit 009407d
Show file tree
Hide file tree
Showing 9 changed files with 1,833 additions and 488 deletions.
55 changes: 51 additions & 4 deletions src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs
Expand Up @@ -202,7 +202,7 @@ private void EmitExpressionCore(BoundExpression expression, bool used)
break;

case BoundKind.IsOperator:
EmitIsExpression((BoundIsOperator)expression, used);
EmitIsExpression((BoundIsOperator)expression, used, omitBooleanConversion: false);
break;

case BoundKind.AsOperator:
Expand Down Expand Up @@ -3158,7 +3158,7 @@ private void EmitPopIfUnused(bool used)
}
}

private void EmitIsExpression(BoundIsOperator isOp, bool used)
private void EmitIsExpression(BoundIsOperator isOp, bool used, bool omitBooleanConversion)
{
var operand = isOp.Operand;
EmitExpression(operand, used);
Expand All @@ -3172,8 +3172,12 @@ private void EmitIsExpression(BoundIsOperator isOp, bool used)
}
_builder.EmitOpCode(ILOpCode.Isinst);
EmitSymbolToken(isOp.TargetType.Type, isOp.Syntax);
_builder.EmitOpCode(ILOpCode.Ldnull);
_builder.EmitOpCode(ILOpCode.Cgt_un);

if (!omitBooleanConversion)
{
_builder.EmitOpCode(ILOpCode.Ldnull);
_builder.EmitOpCode(ILOpCode.Cgt_un);
}
}
}

Expand Down Expand Up @@ -3483,6 +3487,22 @@ private void EmitConditionalOperator(BoundConditionalOperator expr, bool used)
{
Debug.Assert(expr.ConstantValueOpt == null, "Constant value should have been emitted directly");

// Generate branchless IL for (b ? 1 : 0).
if (used && _ilEmitStyle != ILEmitStyle.Debug &&
(IsNumeric(expr.Type) || expr.Type.PrimitiveTypeCode == Cci.PrimitiveTypeCode.Boolean) &&
hasIntegralValueZeroOrOne(expr.Consequence, out var isConsequenceOne) &&
hasIntegralValueZeroOrOne(expr.Alternative, out var isAlternativeOne) &&
isConsequenceOne != isAlternativeOne &&
TryEmitComparison(expr.Condition, sense: isConsequenceOne))
{
var toType = expr.Type.PrimitiveTypeCode;
if (toType != Cci.PrimitiveTypeCode.Boolean)
{
_builder.EmitNumericConversion(Cci.PrimitiveTypeCode.Int32, toType, @checked: false);
}
return;
}

object consequenceLabel = new object();
object doneLabel = new object();

Expand Down Expand Up @@ -3547,6 +3567,33 @@ private void EmitConditionalOperator(BoundConditionalOperator expr, bool used)
}

_builder.MarkLabel(doneLabel);

static bool hasIntegralValueZeroOrOne(BoundExpression expr, out bool isOne)
{
if (expr.ConstantValueOpt is { } constantValue)
{
if (constantValue is { IsIntegral: true, UInt64Value: (1 or 0) and var i })
{
isOne = i == 1;
return true;
}

if (constantValue is { IsBoolean: true, BooleanValue: var b })
{
isOne = b;
return true;
}

if (constantValue is { IsChar: true, CharValue: ((char)1 or (char)0) and var c })
{
isOne = c == 1;
return true;
}
}

isOne = false;
return false;
}
}

/// <summary>
Expand Down
66 changes: 59 additions & 7 deletions src/Compilers/CSharp/Portable/CodeGen/EmitOperators.cs
Expand Up @@ -471,13 +471,7 @@ private void EmitBinaryCondOperatorHelper(ILOpCode opCode, BoundExpression left,
// this will leave a value on the stack which conforms to sense, ie:(condition == sense)
private void EmitCondExpr(BoundExpression condition, bool sense)
{
while (condition.Kind == BoundKind.UnaryOperator)
{
var unOp = (BoundUnaryOperator)condition;
Debug.Assert(unOp.OperatorKind == UnaryOperatorKind.BoolLogicalNegation);
condition = unOp.Operand;
sense = !sense;
}
RemoveNegation(ref condition, ref sense);

Debug.Assert(condition.Type.SpecialType == SpecialType.System_Boolean);

Expand Down Expand Up @@ -506,6 +500,64 @@ private void EmitCondExpr(BoundExpression condition, bool sense)
return;
}

/// <summary>
/// Emits boolean expression without branching if possible (i.e., no logical operators, only comparisons).
/// Leaves a boolean (int32, 0 or 1) value on the stack which conforms to sense, i.e., <c>condition == sense</c>.
/// </summary>
private bool TryEmitComparison(BoundExpression condition, bool sense)
{
RemoveNegation(ref condition, ref sense);

Debug.Assert(condition.Type.SpecialType == SpecialType.System_Boolean);

if (condition.ConstantValueOpt is { } constantValue)
{
Debug.Assert(constantValue.Discriminator == ConstantValueTypeDiscriminator.Boolean);
_builder.EmitBoolConstant(constantValue.BooleanValue == sense);
return true;
}

if (condition is BoundBinaryOperator binOp)
{
// Intentionally don't optimize logical operators, they need branches to short-circuit.
if (binOp.OperatorKind.IsComparison())
{
EmitBinaryCondOperator(binOp, sense: sense);
return true;
}
}
else if (condition is BoundIsOperator isOp)
{
EmitIsExpression(isOp, used: true, omitBooleanConversion: true);

// Convert to 1 or 0.
_builder.EmitOpCode(ILOpCode.Ldnull);
_builder.EmitOpCode(sense ? ILOpCode.Cgt_un : ILOpCode.Ceq);
return true;
}
else
{
EmitExpression(condition, used: true);

// Convert to 1 or 0 (although `condition` is of type `bool`, it can contain any integer).
_builder.EmitOpCode(ILOpCode.Ldc_i4_0);
_builder.EmitOpCode(sense ? ILOpCode.Cgt_un : ILOpCode.Ceq);
return true;
}

return false;
}

private static void RemoveNegation(ref BoundExpression condition, ref bool sense)
{
while (condition is BoundUnaryOperator unOp)
{
Debug.Assert(unOp.OperatorKind == UnaryOperatorKind.BoolLogicalNegation);
condition = unOp.Operand;
sense = !sense;
}
}

private void EmitUnaryCheckedOperatorExpression(BoundUnaryOperator expression, bool used)
{
Debug.Assert(expression.OperatorKind.Operator() == UnaryOperatorKind.UnaryMinus);
Expand Down

0 comments on commit 009407d

Please sign in to comment.