`alignmentMask` and `roundUp/DownToAlignment` for `MemoryLayout`

When doing low-level memory manipulation, it's commonly necessary to align memory addresses. For this purpose, alignment masks (the alignment minus one) are often more efficient and useful than the alignment value itself, since it allows for efficient rounding up and down to the alignment by bit manipulation:

foo = foo & ~7 // round down to nearest 8-byte boundary
foo = (foo + 7) & ~7 // round up to nearest 8-byte boundary

It'd be nice if MemoryLayout provided access to the mask directly, along with alignment-rounding operations on Int, UInt, and UnsafeRawPointer:

extension MemoryLayout {
  var alignmentMask: Int { return alignment - 1 }

% for type in ['Int', 'UInt', 'UnsafeRawPointer', 'UnsafeMutableRawPointer']:
  func roundedUpToAlignment(_ value: ${type}) -> ${type} { ... }
  func roundedDownToAlignment(_ value: ${type}) -> ${type} { ... }

  // or maybe:
  func roundUpToAlignment(_ value: inout ${type}) { ... }
  func roundDownToAlignment(_ value: inout ${type}) { ... }
% end
}

I suppose one could argue that the integer rounding forms could be satisfied by a more general rounding API on Int (and in common cases the optimizer ought to clean up something like integer.rounded(downTo: 8) into bitwise arithmetic), but I think the operations on the pointer types would be particularly useful, since they currently otherwise require bouncing back and forth between integer bit pattern representations first.

8 Likes

In favor of the idea, picky about the names for the convenience functions:

MemoryLayout<Double>.roundedUpToAlignment(ptr)

That sounds like it's the MemoryLayout that's changing based on the pointer, rather than the other way around. You could rephrase the names:

MemoryLayout<Double>.pointerByRoundingUpToAlignment(ptr)

but it might be less awkward to just put this on UnsafeRawPointer itself:

ptr.roundedUpToAlignment(of: Double.self)

(Sorry for starting the bikeshedding this early.)

2 Likes

It's a fair question whether these belong as methods on the affected types. We've been using MemoryLayout as a containment zone so that this kind of low-level stuff doesn't normally pollute the primary API of types; one could argue that it makes sense as part of the primary API of raw pointers, but it'd be nice too for the API on pointers and integers to be uniform.

2 Likes

There seems to be so much replication between Unsafe(Mutable)?(Buffer)?Pointer. Can't methods like this be added to a protocol PointerType, to which all can conform? That could also define the + operators for adding Int offsets, and other stuff like that.

I think that's a broader design question. These operations at least would IMO only make sense on raw pointers, since a typed pointer must already always be aligned appropriately for its type.

3 Likes

True!

could it be spelled like

MemoryLayout<Double>.align(up: pointer)
MemoryLayout<Double>.align(down: pointer)

?

these are static functions so i don’t think the “rounded”/“aligned” convention applies here

In many cases, it's important to round integral offsets rather than rounding actual pointer values. Rounding a pointer value is useful if you've allocated a slab of memory of unknown alignment and need to find a properly-aligned chunk of memory within it. Rounding an offset is what you do if you're trying to match consistent layout rules, like doing dynamic structure layout.

2 Likes

My specific use case for rounding up raw pointers is for scanning forward through an UnsafeRawBufferPointer where I know the next value I want to "popFront" off of it is at a specific alignment.