Skip to content

Commit dbfc427

Browse files
Merged PR 680832: [BuildXL Perf] Reduce the size of EnvironmentVariable struct from 48 to 32
An array of `EnvironmentVariable` structs is created per pip and such arrays occupy quite a bit of memory. Here is an example from bxl dump for COSINE: ``` 305412 2828539152 BuildXL.Pips.Operations.EnvironmentVariable[] ``` I.e. the arrays are almost 3Gb. This PR reduces the size of `EnvironmentVariable` struct from 48 to 32 bytes by flattening types used in it. It also adds a reference to `ObjectLayoutInspector` nuget package that allows inspecting the layout at runtime. The layout before: ![image (2).png](https://dev.azure.com/mseng/9ed2c125-1cd5-4a17-886b-9d267f3a5fab/_apis/git/repositories/50d331c7-ea65-45eb-833f-0303c6c2387e/pullRequests/680832/attachments/image%20%282%29.png) Here is the layout after: ![image.png](https://dev.azure.com/mseng/9ed2c125-1cd5-4a17-886b-9d267f3a5fab/_apis/git/repositories/50d331c7-ea65-45eb-833f-0303c6c2387e/pullRequests/680832/attachments/image.png)
1 parent 2058595 commit dbfc427

File tree

8 files changed

+161
-27
lines changed

8 files changed

+161
-27
lines changed

Public/Src/Pips/Dll/Operations/EnvironmentVariable.cs

+58-6
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,55 @@ namespace BuildXL.Pips.Operations
1212
/// </summary>
1313
public readonly struct EnvironmentVariable : IEquatable<EnvironmentVariable>
1414
{
15+
// This struct used to have the following structure:
16+
// readonly record struct EnvironmentVariable(StringId Name, PipData Value, bool IsPassThrough);
17+
// Unfortunately, due to layout issues, the old version was 48 bytes in size even though we can manually
18+
// pack all the required data into 32 bytes.
19+
20+
// StringId
21+
private readonly int m_nameValue;
22+
23+
// PipData
24+
private readonly bool m_pipDataAvailable;
25+
26+
// PipData.EntriesBinarySegmentPointer
27+
private readonly int m_pipDataEntriesBinarySegmentPointer;
28+
// PipData.HeaderEntry
29+
private readonly PipDataEntryType m_pipDataEntryType;
30+
private readonly PipDataFragmentEscaping m_pipDataEscaping;
31+
private readonly int m_pipDataValue;
32+
// PipData.Entries
33+
private readonly PipDataEntryList m_pipDataEntries;
34+
1535
/// <summary>
1636
/// Name of the variable.
1737
/// </summary>
18-
public readonly StringId Name;
38+
public StringId Name => StringId.UnsafeCreateFrom(m_nameValue);
1939

2040
/// <summary>
2141
/// Value of the variable.
2242
/// </summary>
23-
public readonly PipData Value;
43+
public PipData Value
44+
{
45+
get
46+
{
47+
if (!m_pipDataAvailable)
48+
{
49+
return PipData.Invalid;
50+
}
51+
52+
return PipData.CreateInternal(
53+
new PipDataEntry(m_pipDataEscaping, m_pipDataEntryType, m_pipDataValue),
54+
m_pipDataEntries,
55+
StringId.UnsafeCreateFrom(m_pipDataEntriesBinarySegmentPointer));
56+
}
57+
}
2458

2559
/// <summary>
2660
/// Whether this is a pass-through environment variable
2761
/// </summary>
28-
public readonly bool IsPassThrough;
29-
62+
public bool IsPassThrough { get; }
63+
3064
/// <summary>
3165
/// Creates an environment variable definition.
3266
/// </summary>
@@ -35,8 +69,26 @@ public EnvironmentVariable(StringId name, PipData value, bool isPassThrough = fa
3569
Contract.Requires(name.IsValid);
3670
Contract.Requires(value.IsValid || isPassThrough);
3771

38-
Name = name;
39-
Value = value;
72+
m_nameValue = name.Value;
73+
if (value.IsValid)
74+
{
75+
m_pipDataAvailable = true;
76+
m_pipDataEntriesBinarySegmentPointer = value.EntriesBinarySegmentPointer.Value;
77+
m_pipDataEntryType = value.HeaderEntry.EntryType;
78+
m_pipDataEscaping = value.HeaderEntry.RawEscaping;
79+
m_pipDataValue = value.HeaderEntry.RawData;
80+
m_pipDataEntries = value.Entries;
81+
}
82+
else
83+
{
84+
m_pipDataAvailable = false;
85+
m_pipDataEntriesBinarySegmentPointer = default;
86+
m_pipDataEntryType = default;
87+
m_pipDataEscaping = default;
88+
m_pipDataValue = default;
89+
m_pipDataEntries = default;
90+
}
91+
4092
IsPassThrough = isPassThrough;
4193
}
4294

Public/Src/Pips/Dll/Operations/PipData.cs

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ namespace BuildXL.Pips.Operations
5959

6060
internal readonly PipDataEntry HeaderEntry;
6161
internal readonly PipDataEntryList Entries;
62+
internal StringId EntriesBinarySegmentPointer => m_entriesBinarySegmentPointer;
6263

6364
private PipData(PipDataEntry entry, PipDataEntryList entries, StringId entriesBinarySegmentPointer)
6465
{

Public/Src/Pips/Dll/Operations/PipDataBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace BuildXL.Pips.Operations
1515
/// </summary>
1616
/// <remarks>
1717
/// Representation of PipData is now a sequence of <see cref="PipDataEntry"/> values.
18-
/// Each value is comprised of a single byte code and a 4 byte integer <see cref="PipDataEntry.m_data"/>.
18+
/// Each value is comprised of a single byte code and a 4 byte integer <see cref="PipDataEntry.RawData"/>.
1919
/// The code stores the <see cref="PipDataEntryType"/> and optionally the <see cref="PipDataFragmentEscaping"/>.
2020
///
2121
/// Entry types and interpretation:

Public/Src/Pips/Dll/Operations/PipDataEntry.cs

+23-20
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ namespace BuildXL.Pips.Operations
2424
/// </summary>
2525
public PipDataEntryType EntryType { get; }
2626

27-
private readonly PipDataFragmentEscaping m_escaping;
28-
private readonly int m_data;
27+
/// <nodoc />
28+
internal readonly PipDataFragmentEscaping RawEscaping;
29+
30+
/// <nodoc />
31+
internal readonly int RawData;
2932

3033
/// <nodoc />
3134
public PipDataEntry(PipDataFragmentEscaping escaping, PipDataEntryType entryType, uint data)
@@ -40,8 +43,8 @@ public PipDataEntry(PipDataFragmentEscaping escaping, PipDataEntryType entryType
4043
{
4144
Contract.Requires(escaping == PipDataFragmentEscaping.Invalid || entryType == PipDataEntryType.NestedDataHeader);
4245
EntryType = entryType;
43-
m_escaping = escaping;
44-
m_data = data;
46+
RawEscaping = escaping;
47+
RawData = data;
4548
}
4649

4750
/// <summary>
@@ -55,7 +58,7 @@ public PipDataFragmentEscaping Escaping
5558
get
5659
{
5760
Contract.Requires(EntryType == PipDataEntryType.NestedDataHeader);
58-
return m_escaping;
61+
return RawEscaping;
5962
}
6063
}
6164

@@ -99,7 +102,7 @@ public PipFragmentType FragmentType
99102
public AbsolutePath GetPathValue()
100103
{
101104
Contract.Requires(EntryType == PipDataEntryType.AbsolutePath || EntryType == PipDataEntryType.VsoHashEntry1Path || EntryType == PipDataEntryType.FileId1Path);
102-
return new AbsolutePath(m_data);
105+
return new AbsolutePath(RawData);
103106
}
104107

105108
/// <summary>
@@ -117,7 +120,7 @@ public int GetIntegralValue()
117120
EntryType == PipDataEntryType.NestedDataEnd ||
118121
EntryType == PipDataEntryType.VsoHashEntry2RewriteCount ||
119122
EntryType == PipDataEntryType.FileId2RewriteCount);
120-
return m_data;
123+
return RawData;
121124
}
122125

123126
/// <summary>
@@ -131,7 +134,7 @@ public uint GetUInt32Value()
131134
{
132135
Contract.Requires(
133136
EntryType == PipDataEntryType.DirectoryIdHeaderSealId);
134-
return unchecked((uint)m_data);
137+
return unchecked((uint)RawData);
135138
}
136139

137140
/// <summary>
@@ -148,7 +151,7 @@ public StringId GetStringValue()
148151
EntryType == PipDataEntryType.StringLiteral ||
149152
EntryType == PipDataEntryType.NestedDataHeader ||
150153
EntryType == PipDataEntryType.IpcMoniker);
151-
return new StringId(m_data);
154+
return new StringId(RawData);
152155
}
153156

154157
#region Conversions
@@ -244,8 +247,8 @@ public static implicit operator PipDataEntry(AbsolutePath data)
244247

245248
public void Write(byte[] bytes, ref int index)
246249
{
247-
bytes[index++] = checked((byte)(((int)EntryType << 4) | (int)m_escaping));
248-
Bits.WriteInt32(bytes, ref index, m_data);
250+
bytes[index++] = checked((byte)(((int)EntryType << 4) | (int)RawEscaping));
251+
Bits.WriteInt32(bytes, ref index, RawData);
249252
}
250253

251254
public static PipDataEntry Read<TBytes>(TBytes bytes, ref int index)
@@ -264,28 +267,28 @@ public void Serialize(BuildXLWriter writer)
264267
{
265268
Contract.Requires(writer != null);
266269
Contract.Assert((int)EntryType < 16);
267-
Contract.Assert((int)m_escaping < 16);
268-
writer.Write(checked((byte)(((int)EntryType << 4) | (int)m_escaping)));
270+
Contract.Assert((int)RawEscaping < 16);
271+
writer.Write(checked((byte)(((int)EntryType << 4) | (int)RawEscaping)));
269272
switch (EntryType)
270273
{
271274
case PipDataEntryType.NestedDataHeader:
272275
case PipDataEntryType.StringLiteral:
273-
writer.Write(new StringId(m_data));
276+
writer.Write(new StringId(RawData));
274277
break;
275278
case PipDataEntryType.NestedDataStart:
276279
case PipDataEntryType.NestedDataEnd:
277280
case PipDataEntryType.VsoHashEntry2RewriteCount:
278281
case PipDataEntryType.FileId2RewriteCount:
279282
case PipDataEntryType.DirectoryIdHeaderSealId:
280-
writer.WriteCompact(m_data);
283+
writer.WriteCompact(RawData);
281284
break;
282285
case PipDataEntryType.AbsolutePath:
283286
case PipDataEntryType.VsoHashEntry1Path:
284287
case PipDataEntryType.FileId1Path:
285-
writer.Write(new AbsolutePath(m_data));
288+
writer.Write(new AbsolutePath(RawData));
286289
break;
287290
case PipDataEntryType.IpcMoniker:
288-
writer.Write(new StringId(m_data));
291+
writer.Write(new StringId(RawData));
289292
break;
290293
default:
291294
Contract.Assert(false, "EntryType not handled: " + EntryType);
@@ -340,7 +343,7 @@ public static PipDataEntry Deserialize(BuildXLReader reader)
340343
/// <inheritdoc />
341344
public override int GetHashCode()
342345
{
343-
return HashCodeHelper.Combine((int)EntryType, (int)m_escaping, m_data);
346+
return HashCodeHelper.Combine((int)EntryType, (int)RawEscaping, RawData);
344347
}
345348

346349
/// <inheritdoc />
@@ -353,8 +356,8 @@ public override bool Equals(object obj)
353356
public bool Equals(PipDataEntry other)
354357
{
355358
return other.EntryType == EntryType &&
356-
other.m_data == m_data &&
357-
other.m_escaping == m_escaping;
359+
other.RawData == RawData &&
360+
other.RawEscaping == RawEscaping;
358361
}
359362

360363
public static bool operator ==(PipDataEntry left, PipDataEntry right)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using BuildXL.Pips.Operations;
5+
using BuildXL.Utilities;
6+
using Test.BuildXL.TestUtilities.Xunit;
7+
using Xunit;
8+
using Xunit.Abstractions;
9+
10+
namespace Test.BuildXL.Pips
11+
{
12+
public sealed class EnvironmentVariableLayoutTests : XunitBuildXLTest
13+
{
14+
private readonly ITestOutputHelper _output;
15+
public EnvironmentVariableLayoutTests(ITestOutputHelper output)
16+
: base(output)
17+
{
18+
_output = output;
19+
}
20+
21+
[Fact]
22+
public void PipDataStoredCorrectly()
23+
{
24+
// EnvironmentVariable flattens PipData structure, so its important to make sure
25+
// that if PipData structure changes EnvironmentVariable is changed as well.
26+
// This test ensures the consistency.
27+
var pipData = PipData.Invalid;
28+
var envVar = new EnvironmentVariable(StringId.UnsafeCreateFrom(42), pipData, isPassThrough: true);
29+
Assert.Equal(pipData, envVar.Value);
30+
31+
var pipDataEntry = new PipDataEntry(PipDataFragmentEscaping.NoEscaping, PipDataEntryType.NestedDataHeader, 42);
32+
pipData = PipData.CreateInternal(
33+
pipDataEntry,
34+
PipDataEntryList.FromEntries(new[] {pipDataEntry}),
35+
StringId.UnsafeCreateFrom(1));
36+
envVar = new EnvironmentVariable(StringId.UnsafeCreateFrom(42), pipData, isPassThrough: false);
37+
Assert.Equal(pipData, envVar.Value);
38+
}
39+
40+
[Fact]
41+
public void EnvironmentVariableSizeIs32()
42+
{
43+
// EnvironmentVariable structs are stored for every pip and reducing the size of such structs
44+
// reasonably reduces the peak memory consumption.
45+
46+
// Flattening the layout saves 30% of space compared to the naive old version.
47+
var layout = ObjectLayoutInspector.TypeLayout.GetLayout<EnvironmentVariable>();
48+
_output.WriteLine(layout.ToString());
49+
50+
Assert.Equal(32, layout.Size);
51+
var oldLayout = ObjectLayoutInspector.TypeLayout.GetLayout<OldEnvironmentVariable>();
52+
_output.WriteLine(oldLayout.ToString());
53+
Assert.True(layout.Size < oldLayout.Size);
54+
}
55+
56+
// Using StringIdStable and not StringId, because StringId layout is different for debug builds.
57+
private record struct OldEnvironmentVariable(StringIdStable Name, PipData Value, bool IsPassThrough);
58+
59+
private record struct StringIdStable(int Value);
60+
}
61+
}

Public/Src/Pips/UnitTests/Pips/Test.BuildXL.Pips.dsc

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ namespace Core {
77
assemblyName: "Test.BuildXL.Pips",
88
sources: globR(d`.`, "*.cs"),
99
references: [
10+
...addIf(
11+
BuildXLSdk.isFullFramework,
12+
NetFx.Netstandard.dll
13+
),
14+
15+
importFrom("ObjectLayoutInspector").pkg,
1016
importFrom("BuildXL.Cache.ContentStore").Hashing.dll,
1117
importFrom("BuildXL.Cache.ContentStore").UtilitiesCore.dll,
1218
importFrom("BuildXL.Cache.ContentStore").Interfaces.dll,

cg/nuget/cgmanifest.json

+9
Original file line numberDiff line numberDiff line change
@@ -2809,6 +2809,15 @@
28092809
}
28102810
}
28112811
},
2812+
{
2813+
"Component": {
2814+
"Type": "NuGet",
2815+
"NuGet": {
2816+
"Name": "ObjectLayoutInspector",
2817+
"Version": "0.1.4"
2818+
}
2819+
}
2820+
},
28122821
{
28132822
"Component": {
28142823
"Type": "NuGet",

config.dsc

+2
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ config({
340340

341341
{ id: "SharpZipLib", version: "1.3.3" },
342342

343+
{ id: "ObjectLayoutInspector", version: "0.1.4" },
344+
343345
// Ninja JSON graph generation helper
344346
{ id: "BuildXL.Tools.Ninjson", version: "0.0.6" },
345347
{ id: "BuildXL.Tools.AppHostPatcher", version: "1.0.0" },

0 commit comments

Comments
 (0)