Skip to content

Commit 1ee3813

Browse files
feat: Improves enum niche-filling variant packing
This feature only works with "-Zunstable-options --edition future" as it changes the rustc_abi. When a variant cannot fit as a whole before or after the reserved niche, try repacking its fields around the niche instead. This makes niche-filling less conservative while preserving the existing layout behavior when whole-variant placement already succeeds. Signed-off-by: Miguel Marques <miguel.m.marques@tecnico.ulisboa.pt> Co-authored-by: Vicente Gusmão <vicente.gusmao@tecnico.ulisboa.pt>
1 parent ab26b17 commit 1ee3813

4 files changed

Lines changed: 460 additions & 2 deletions

File tree

compiler/rustc_abi/src/layout.rs

Lines changed: 307 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,198 @@ impl<Cx: HasDataLayout> LayoutCalculator<Cx> {
742742
Some(layout)
743743
};
744744

745-
let niche_filling_layout = calculate_niche_filling_layout();
745+
let calculate_niche_filling_layout_repacked =
746+
|| -> Option<LayoutData<FieldIdx, VariantIdx>> {
747+
struct VariantLayoutInfo {
748+
align_abi: Align,
749+
}
750+
751+
if repr.inhibit_enum_layout_opt() {
752+
return None;
753+
}
754+
755+
if variants.len() < 2 {
756+
return None;
757+
}
758+
759+
let mut align = dl.aggregate_align;
760+
let mut max_repr_align = repr.align;
761+
let mut unadjusted_abi_align = align;
762+
let mut combined_seed = repr.field_shuffle_seed;
763+
764+
let mut variants_info = IndexVec::<VariantIdx, _>::with_capacity(variants.len());
765+
let variant_layouts = variants
766+
.iter()
767+
.map(|v| {
768+
let st = self.univariant(v, repr, StructKind::AlwaysSized).ok()?;
769+
770+
variants_info.push(VariantLayoutInfo { align_abi: st.align.abi });
771+
772+
align = align.max(st.align.abi);
773+
max_repr_align = max_repr_align.max(st.max_repr_align);
774+
unadjusted_abi_align = unadjusted_abi_align.max(st.unadjusted_abi_align);
775+
combined_seed = combined_seed.wrapping_add(st.randomization_seed);
776+
777+
Some(VariantLayout::from_layout(st))
778+
})
779+
.collect::<Option<IndexVec<VariantIdx, _>>>()?;
780+
781+
let max_variant_size = variant_layouts.iter().map(|layout| layout.size).max()?;
782+
783+
// Chooses the first max-sized niche-providing variant whose layout succeeds.
784+
for (largest_variant_index, _) in
785+
variant_layouts.iter_enumerated().filter(|&(_, layout)| {
786+
layout.size == max_variant_size && layout.largest_niche.is_some()
787+
})
788+
{
789+
let mut variant_layouts = variant_layouts.clone();
790+
791+
let all_indices = variants.indices();
792+
let needs_disc = |index: VariantIdx| {
793+
index != largest_variant_index && !absent(&variants[index])
794+
};
795+
let Some(niche_variants_start) = all_indices.clone().find(|v| needs_disc(*v))
796+
else {
797+
continue;
798+
};
799+
let Some(niche_variants_end) = all_indices.rev().find(|v| needs_disc(*v))
800+
else {
801+
continue;
802+
};
803+
let niche_variants = niche_variants_start..=niche_variants_end;
804+
805+
let count = (niche_variants.end().index() as u128
806+
- niche_variants.start().index() as u128)
807+
+ 1;
808+
809+
// Use the largest niche in the largest variant.
810+
let Some(niche) = variant_layouts[largest_variant_index].largest_niche else {
811+
continue;
812+
};
813+
let Some((niche_start, niche_scalar)) = niche.reserve(dl, count) else {
814+
continue;
815+
};
816+
let niche_offset = niche.offset;
817+
let niche_size = niche.value.size(dl);
818+
let size = variant_layouts[largest_variant_index].size.align_to(align);
819+
820+
let all_variants_fit =
821+
variant_layouts.iter_enumerated_mut().all(|(i, layout)| {
822+
if i == largest_variant_index {
823+
return true;
824+
}
825+
826+
layout.largest_niche = None;
827+
828+
if layout.size <= niche_offset {
829+
// This variant will fit before the niche.
830+
return true;
831+
}
832+
833+
// Determine if it'll fit after the niche.
834+
let this_align = variants_info[i].align_abi;
835+
let this_offset = (niche_offset + niche_size).align_to(this_align);
836+
837+
if this_offset + layout.size > size {
838+
// The ordinary niche-filling path can only move a non-largest variant as a
839+
// whole before or after the chosen niche. If that fails, try placing the
840+
// variant's fields individually while treating the niche bytes as reserved.
841+
if let Some(repacked) = self.try_layout_variant_around_niche(
842+
&variants[i],
843+
repr,
844+
i,
845+
size,
846+
align,
847+
niche_offset,
848+
niche_size,
849+
) {
850+
*layout = VariantLayout::from_layout(repacked);
851+
return true;
852+
}
853+
return false;
854+
}
855+
856+
// It'll fit, but we need to make some adjustments.
857+
for offset in layout.field_offsets.iter_mut() {
858+
*offset += this_offset;
859+
}
860+
861+
// It can't be a Scalar or ScalarPair because the offset isn't 0.
862+
if !layout.is_uninhabited() {
863+
layout.backend_repr = BackendRepr::Memory { sized: true };
864+
}
865+
layout.size += this_offset;
866+
867+
true
868+
});
869+
870+
if !all_variants_fit {
871+
continue;
872+
}
873+
874+
let largest_niche = Niche::from_scalar(dl, niche_offset, niche_scalar);
875+
876+
let others_zst = variant_layouts
877+
.iter_enumerated()
878+
.all(|(i, layout)| i == largest_variant_index || layout.size == Size::ZERO);
879+
let same_size = size == variant_layouts[largest_variant_index].size;
880+
let same_align = align == variants_info[largest_variant_index].align_abi;
881+
882+
let uninhabited = variant_layouts.iter().all(|v| v.is_uninhabited());
883+
let abi = if same_size && same_align && others_zst {
884+
match variant_layouts[largest_variant_index].backend_repr {
885+
// When the total alignment and size match, we can use the
886+
// same ABI as the scalar variant with the reserved niche.
887+
BackendRepr::Scalar(_) => BackendRepr::Scalar(niche_scalar),
888+
BackendRepr::ScalarPair(first, second) => {
889+
// Only the niche is guaranteed to be initialised,
890+
// so use union layouts for the other primitive.
891+
if niche_offset == Size::ZERO {
892+
BackendRepr::ScalarPair(niche_scalar, second.to_union())
893+
} else {
894+
BackendRepr::ScalarPair(first.to_union(), niche_scalar)
895+
}
896+
}
897+
_ => BackendRepr::Memory { sized: true },
898+
}
899+
} else {
900+
BackendRepr::Memory { sized: true }
901+
};
902+
903+
return Some(LayoutData {
904+
variants: Variants::Multiple {
905+
tag: niche_scalar,
906+
tag_encoding: TagEncoding::Niche {
907+
untagged_variant: largest_variant_index,
908+
niche_variants,
909+
niche_start,
910+
},
911+
tag_field: FieldIdx::new(0),
912+
variants: variant_layouts,
913+
},
914+
fields: FieldsShape::Arbitrary {
915+
offsets: [niche_offset].into(),
916+
in_memory_order: [FieldIdx::new(0)].into(),
917+
},
918+
backend_repr: abi,
919+
largest_niche,
920+
uninhabited,
921+
size,
922+
align: AbiAlign::new(align),
923+
max_repr_align,
924+
unadjusted_abi_align,
925+
randomization_seed: combined_seed,
926+
});
927+
}
928+
929+
None
930+
};
931+
932+
let niche_filling_layout = if repr.can_repack_variant_around_niche() {
933+
calculate_niche_filling_layout_repacked()
934+
} else {
935+
calculate_niche_filling_layout()
936+
};
746937

747938
let discr_type = repr.discr_type();
748939
let discr_int = Integer::from_attr(dl, discr_type);
@@ -1091,6 +1282,121 @@ impl<Cx: HasDataLayout> LayoutCalculator<Cx> {
10911282
Ok(best_layout)
10921283
}
10931284

1285+
fn try_layout_variant_around_niche<
1286+
'a,
1287+
FieldIdx: Idx,
1288+
VariantIdx: Idx,
1289+
F: Deref<Target = &'a LayoutData<FieldIdx, VariantIdx>> + fmt::Debug + Copy,
1290+
>(
1291+
&self,
1292+
fields: &IndexSlice<FieldIdx, F>,
1293+
repr: &ReprOptions,
1294+
variant_index: VariantIdx,
1295+
total_size: Size,
1296+
total_align: Align,
1297+
niche_offset: Size,
1298+
niche_size: Size,
1299+
) -> Option<LayoutData<FieldIdx, VariantIdx>> {
1300+
let dl = self.cx.data_layout();
1301+
1302+
if repr.inhibit_struct_field_reordering() {
1303+
return None;
1304+
}
1305+
if fields.iter().any(|f| f.is_unsized()) {
1306+
return None;
1307+
}
1308+
1309+
// This is only a necessary condition: alignment can still make placement fail below.
1310+
let min_used_size = fields.iter().try_fold(niche_size, |size, field| {
1311+
if field.is_zst() { Some(size) } else { size.checked_add(field.size, dl) }
1312+
})?;
1313+
if min_used_size > total_size {
1314+
return None;
1315+
}
1316+
1317+
let mut offsets = IndexVec::from_elem(Size::ZERO, fields);
1318+
let mut in_memory_order: IndexVec<u32, FieldIdx> = fields.indices().collect();
1319+
1320+
in_memory_order.raw.sort_by_key(|&i| {
1321+
let field = &fields[i];
1322+
1323+
let field_align = if let Some(pack) = repr.pack {
1324+
field.align.min(AbiAlign::new(pack))
1325+
} else {
1326+
field.align
1327+
};
1328+
1329+
(cmp::Reverse(field_align.abi.bytes()), cmp::Reverse(field.size.bytes()))
1330+
});
1331+
1332+
let niche_end = niche_offset.checked_add(niche_size, dl)?;
1333+
let mut occupied = vec![(niche_offset, niche_end)];
1334+
1335+
let mut max_repr_align = repr.align;
1336+
let mut unadjusted_abi_align =
1337+
if repr.pack.is_some() { dl.i8_align } else { dl.aggregate_align };
1338+
1339+
for &i in &in_memory_order {
1340+
let field = &fields[i];
1341+
1342+
let field_align = if let Some(pack) = repr.pack {
1343+
field.align.min(AbiAlign::new(pack))
1344+
} else {
1345+
field.align
1346+
};
1347+
1348+
max_repr_align = max_repr_align.max(field.max_repr_align);
1349+
unadjusted_abi_align = unadjusted_abi_align.max(field_align.abi);
1350+
1351+
let mut candidate = Size::ZERO;
1352+
'search: loop {
1353+
candidate = candidate.align_to(field_align.abi);
1354+
1355+
let end = candidate.checked_add(field.size, dl)?;
1356+
if end > total_size {
1357+
return None;
1358+
}
1359+
1360+
for &(occupied_start, occupied_end) in &occupied {
1361+
if end <= occupied_start {
1362+
break;
1363+
}
1364+
if candidate < occupied_end {
1365+
candidate = occupied_end;
1366+
continue 'search;
1367+
}
1368+
}
1369+
1370+
offsets[i] = candidate;
1371+
1372+
if !field.is_zst() {
1373+
occupied.push((candidate, end));
1374+
occupied.sort_by_key(|&(start, _end)| start);
1375+
}
1376+
1377+
break;
1378+
}
1379+
}
1380+
1381+
in_memory_order.raw.sort_by_key(|&i| offsets[i]);
1382+
1383+
Some(LayoutData {
1384+
variants: Variants::Single { index: variant_index },
1385+
fields: FieldsShape::Arbitrary { offsets, in_memory_order },
1386+
backend_repr: BackendRepr::Memory { sized: true },
1387+
largest_niche: None,
1388+
uninhabited: fields.iter().any(|f| f.is_uninhabited()),
1389+
align: AbiAlign::new(total_align),
1390+
size: total_size,
1391+
max_repr_align,
1392+
unadjusted_abi_align,
1393+
randomization_seed: fields
1394+
.iter()
1395+
.fold(Hash64::ZERO, |acc, f| acc.wrapping_add(f.randomization_seed))
1396+
.wrapping_add(repr.field_shuffle_seed),
1397+
})
1398+
}
1399+
10941400
fn univariant_biased<
10951401
'a,
10961402
FieldIdx: Idx,

compiler/rustc_abi/src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ bitflags! {
9494
/// See [`TyAndLayout::pass_indirectly_in_non_rustic_abis`] for details.
9595
const PASS_INDIRECTLY_IN_NON_RUSTIC_ABIS = 1 << 5;
9696
const IS_SCALABLE = 1 << 6;
97-
// Any of these flags being set prevent field reordering optimisation.
97+
/// If true, enum niche-filling may repack variant fields around the reserved niche.
98+
const CAN_REPACK_VARIANT_AROUND_NICHE = 1 << 7;
99+
// Any of these flags being set prevent field reordering optimisation.
98100
const FIELD_ORDER_UNOPTIMIZABLE = ReprFlags::IS_C.bits()
99101
| ReprFlags::IS_SIMD.bits()
100102
| ReprFlags::IS_SCALABLE.bits()
@@ -226,6 +228,12 @@ impl ReprOptions {
226228
!self.inhibit_struct_field_reordering() && self.flags.contains(ReprFlags::RANDOMIZE_LAYOUT)
227229
}
228230

231+
/// Returns `true` if enum niche-filling can try to pack variant fields around
232+
/// the reserved niche when whole-variant placement fails.
233+
pub fn can_repack_variant_around_niche(&self) -> bool {
234+
self.flags.contains(ReprFlags::CAN_REPACK_VARIANT_AROUND_NICHE)
235+
}
236+
229237
/// Returns `true` if this `#[repr()]` should inhibit union ABI optimisations.
230238
pub fn inhibits_union_abi_opt(&self) -> bool {
231239
self.c()

compiler/rustc_middle/src/ty/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,6 +1603,10 @@ impl<'tcx> TyCtxt<'tcx> {
16031603
flags.insert(ReprFlags::RANDOMIZE_LAYOUT);
16041604
}
16051605

1606+
if self.def_span(did).edition().at_least_edition_future() {
1607+
flags.insert(ReprFlags::CAN_REPACK_VARIANT_AROUND_NICHE);
1608+
}
1609+
16061610
// box is special, on the one hand the compiler assumes an ordered layout, with the pointer
16071611
// always at offset zero. On the other hand we want scalar abi optimizations.
16081612
let is_box = self.is_lang_item(did.to_def_id(), LangItem::OwnedBox);

0 commit comments

Comments
 (0)