- ISPMDLane: add MaskGather, MaskStore, Scatter, MaskScatter; update MaskLoad/Gather signatures for hardware parity - WideLane/ScalarLane: implement new methods with HW/fallback logic - MathV: gather/mask-gather now delegate to lane methods - Vector2/3/4: add CompressStore, Scatter, MaskScatter - SPMD jobs/tests/README: migrate to new APIs for correctness - Use Unsafe.BitCast instead of Unsafe.As/AsRef - Add SPMDUtility for gather index extraction - Job system: add ICustomJob<TSelf>, ScheduleCustom overload - FreeList concurrency obsolete; always thread-safe - NuGet: include LICENSE/README, set license/readme in .csproj - Docs: update SPMD usage, clarify safety notes - Minor: doc fixes, CompressStore test improvements
482 lines
18 KiB
Plaintext
482 lines
18 KiB
Plaintext
<#@ import namespace="System.Text" #>
|
|
<#+
|
|
private const string TLane = "TLane";
|
|
private const string TNumber = "TNumber";
|
|
|
|
private string TLaneRestrictions = $@"where {TLane} : ISPMDLane<{TLane}, {TNumber}>";
|
|
private string TNumberRestrictions = $@"where {TNumber} : unmanaged, INumber<{TNumber}>, IBinaryNumber<{TNumber}>, IMinMaxValue<{TNumber}>, IBitwiseOperators<{TNumber}, {TNumber}, {TNumber}>";
|
|
|
|
private int[] dimensions = new int[] { 2, 3, 4 };
|
|
private char[] components = new char[] { 'x', 'y', 'z', 'w' };
|
|
|
|
public string Indent(string str, int spaces)
|
|
{
|
|
var indent = new string(' ', spaces);
|
|
return indent + str;
|
|
}
|
|
|
|
private string ForEachDimension(int maxDimension, int indentation, string spliter, Action<int, StringBuilder> action, bool spliterOnFirst = true)
|
|
{
|
|
var sb = new StringBuilder();
|
|
for (var dimension = 0; dimension < maxDimension; dimension++)
|
|
{
|
|
if ((dimension > 0 || spliterOnFirst) && indentation > 0)
|
|
{
|
|
sb.Append(new string(' ', indentation));
|
|
}
|
|
|
|
action(dimension, sb);
|
|
if (dimension < maxDimension - 1)
|
|
{
|
|
sb.Append(spliter);
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public StringBuilder GenerateVectorType(int dimension)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
var typeName = $"Vector{dimension}<{TLane}, {TNumber}>";
|
|
|
|
sb.AppendLine(@$"
|
|
public unsafe struct {typeName} : IEquatable<{typeName}>
|
|
{TLaneRestrictions}
|
|
{TNumberRestrictions}
|
|
{{
|
|
{ForEachDimension(dimension, 4, Environment.NewLine, (dim, sb) => sb.Append($"public {TLane} {components[dim]};"))}
|
|
|
|
public static {typeName} Zero
|
|
{{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 16, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = {TLane}.Zero,"))}
|
|
}};
|
|
}}
|
|
}}
|
|
|
|
public static {typeName} One
|
|
{{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 16, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = {TLane}.One,"))}
|
|
}};
|
|
}}
|
|
}}
|
|
|
|
public {TLane} this[int index]
|
|
{{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get
|
|
{{
|
|
RangeCheck(index);
|
|
return Unsafe.Add(ref x, index);
|
|
}}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
[Conditional(""MHP_ENABLE_SAFETY_CHECKS"")]
|
|
private static void RangeCheck(int index)
|
|
{{
|
|
if (index < 0 || index >= {dimension})
|
|
{{
|
|
throw new IndexOutOfRangeException($""Index {{index}} is out of range for Vector{dimension}."");
|
|
}}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Store({TNumber}* pDst)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.Store(pDst + {dim} * TLane.LaneWidth);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Store(ref {TNumber} dst)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.Store(ref Unsafe.Add(ref dst, {dim} * TLane.LaneWidth));"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Store({ForEachDimension(dimension, 1, ",", (dim, sb) => sb.Append($"{TNumber}* p{components[dim]}"), false)})
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.Store(p{components[dim]});"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Store({ForEachDimension(dimension, 1, ",", (dim, sb) => sb.Append($"ref {TNumber} {components[dim]}"), false)})
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"this.{components[dim]}.Store(ref {components[dim]});"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void CompressStore({TNumber}* pDst, {typeName} mask)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.CompressStore(pDst + {dim} * TLane.LaneWidth, mask.{components[dim]});"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void CompressStore(ref {TNumber} dst, {typeName} mask)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.CompressStore(ref Unsafe.Add(ref dst, {dim} * TLane.LaneWidth), mask.{components[dim]});"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Scatter({TNumber}* pDst, {TLane} indices)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.Scatter(pDst + {dim}, indices);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Scatter({TNumber}* pDst, int* pIndices)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.Scatter(pDst + {dim}, pIndices);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Scatter(ref {TNumber} dst, {TLane} indices)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.Scatter(ref Unsafe.Add(ref dst, {dim}), indices);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Scatter(ref {TNumber} dst, int* pIndices)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.Scatter(ref Unsafe.Add(ref dst, {dim}), pIndices);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void MaskScatter({TNumber}* pDst, {TLane} indices, {TLane} mask)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.MaskScatter(pDst + {dim}, indices, mask);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void MaskScatter({TNumber}* pDst, int* pIndices, {TLane} mask)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.MaskScatter(pDst + {dim}, pIndices, mask);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void MaskScatter(ref {TNumber} dst, {TLane} indices, {TLane} mask)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.MaskScatter(ref Unsafe.Add(ref dst, {dim}), indices, mask);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void MaskScatter(ref {TNumber} dst, int* pIndices, {TLane} mask)
|
|
{{
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]}.MaskScatter(ref Unsafe.Add(ref dst, {dim}), pIndices, mask);"))}
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator -(in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = -vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator +(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} + right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator +(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} + lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator +({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane + vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator -(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} - right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator -(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} - lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator -({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane - vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator *(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} * right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator *(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} * lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator *({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane * vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator /(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} / right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator /(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} / lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator /({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane / vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator ==(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} == right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator ==(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} == lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator ==({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane == vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator !=(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} != right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator !=(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} != lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator !=({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane != vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator >(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} > right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator >(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} > lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator >({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane > vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator >=(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} >= right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator >=(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} >= lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator >=({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane >= vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator <(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} < right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator <(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} < lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator <({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane < vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator <=(in {typeName} left, in {typeName} right)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = left.{components[dim]} <= right.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator <=(in {typeName} vector, {TLane} lane)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = vector.{components[dim]} <= lane,"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static {typeName} operator <=({TLane} lane, in {typeName} vector)
|
|
{{
|
|
return new {typeName}
|
|
{{
|
|
{ForEachDimension(dimension, 12, Environment.NewLine, (dim, sb) => sb.Append($"{components[dim]} = lane <= vector.{components[dim]},"))}
|
|
}};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public readonly bool Equals({typeName} other)
|
|
{{
|
|
return {ForEachDimension(dimension, 0, " && ", (dim, sb) => sb.Append($"this.{components[dim]}.Equals(other.{components[dim]})"), false)};
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override readonly bool Equals(object? obj)
|
|
{{
|
|
return obj is {typeName} other && Equals(other);
|
|
}}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override readonly int GetHashCode()
|
|
{{
|
|
var hash = new HashCode();
|
|
{ForEachDimension(dimension, 8, Environment.NewLine, (dim, sb) => sb.Append($"hash.Add({components[dim]});"))}
|
|
return hash.ToHashCode();
|
|
}}
|
|
}}");
|
|
|
|
return sb;
|
|
}
|
|
#> |