18

I'm aware you can use MemoryLayout<T>.size to get the size of a type T.

For example: MemoryLayout<Int32>.size // 4

However, for class instances (objects), MemoryLayout<T>.size returns the size of the reference to the object (8 bytes on 64 bit machines), not the size of the actual objects on the heap.

class ClassA { // Objects should be at least 8 bytes
    let x: Int64 = 0
}

class ClassB  {// Objects should be at least 16 bytes
    let x: Int64 = 0
    let y: Int64 = 0
}

MemoryLayout<ClassA>.size // 8
MemoryLayout<ClassB>.size // 8, as well :(

How can I get the size of the objects themselves?

For those wondering, I have no real need for this, I'm just exploring around Swift and its interoperability with C.

Alexander
  • 59,041
  • 12
  • 98
  • 151

2 Answers2

20

One option on Apple platforms, because Swift classes are currently built on top of Objective-C classes there, would be to use the Obj-C runtime function class_getInstanceSize, which gives you the size in bytes of an instance of the class, including any padding.

// on a 64-bit machine (1 word == 8 bytes)...

import Foundation

class C {}
print(class_getInstanceSize(C.self)) // 16 bytes metadata for empty class 
                                     // (isa ptr + ref count)

class C1 {
    var i = 0
    var i1 = 0
    var b = false
}

print(class_getInstanceSize(C1.self)) // 40 bytes
// (16 metadata + 24 ivars, 8 for i + 8 for i1 + 1 for b + 7 padding)
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • I didnt know non-objc classes use the ObjC runtime. Doesnt that just tank performance? (By of venting inlining and devirtualization?) – Alexander Jul 13 '17 at 14:07
  • 1
    @Alexander Swift actually gets the best of both worlds in that regard, as methods (including property accessors) aren't automatically exposed to Obj-C (but the ivars themselves are, which is why this works). In addition, even for members that are exposed (w/ `@objc`), Swift doesn't by default dispatch to them using message dispatch (crawling through the hierarchy to find the implementation), it uses table dispatch with its own vtables (but will go back to message for `dynamic` members), and in some cases (such as `final`/`private` classes), can statically dispatch and inline. – Hamish Jul 13 '17 at 14:22
  • Interesting. In that case, what use does it get out of using the ObjC runtime at all? For example, if I dynamically add a method to such a class at runtime, how does it know to consult the runtime to check for it, if it's not using objc_msgsend? Do you know of any resources documenting this? – Alexander Jul 13 '17 at 14:55
  • 1
    @Alexander We get fast and simple interoperability with Obj-C – we can just pass off a given class instance to Obj-C without needing to box/copy it (which is very useful considering just how many Apple frameworks are in Obj-C). As said, Swift doesn't by default use message dispatch through the Obj-C runtime for non-`dynamic` members, so if for example you swizzle a method, calling it from [Swift won't call the swizzled implementation](https://gist.github.com/hamishknight/ea961b8aa360a13b9f1cb1a886a6eaab) unless it's marked as `dynamic` (forcing message dispatch). – Hamish Jul 13 '17 at 15:37
  • 1
    As for resources, I'm afraid they're fairly limited; Mike Ash's blog post that I link to is great, but there's also some documentation in the Swift repo, see https://github.com/apple/swift/blob/master/docs/ABI.rst#class-metadata & https://github.com/apple/swift/blob/master/docs/ABIStabilityManifesto.md#class-metadata – Hamish Jul 13 '17 at 15:37
  • @Hasish I understand the case for existing members (must be `dynamic` to be dynamically changed), but what if I add a *new* member? You can't apply `dynamic` to a class, so is it the case that there must exist at least one `dynamic` member, which will necessitate the use of the runtime, and the potential to find newly added selectors? – Alexander Jul 13 '17 at 15:59
  • 1
    @Alexander In the case of adding methods, you have to go through the Obj-C runtime anyway in order to call them (because the compiler doesn't know anything about them), so you'll be getting message dispatch. The dispatch behaviour isn't all or nothing (i.e all class members dispatched through Obj-C runtime, or none are); it's done on a member-by-member basis. – Hamish Jul 13 '17 at 16:02
  • Hah, you're right! I didn't consider that you would need to go through the runtime to invoke such a call. Thanks for the info! – Alexander Jul 13 '17 at 16:07
  • @Alexander No worries! :) – Hamish Jul 13 '17 at 16:10
  • Note that this approach won't account for the contents of any child object, such as a really large String. This is because the method operates on types, not instances. – mrgrieves Jul 12 '22 at 02:30
17

As far as I tested in the Playground, this function returns seemingly significant value:

func heapSize(_ obj: AnyObject) -> Int {
    return malloc_size(Unmanaged.passUnretained(obj).toOpaque())
}

class MyClass {
    //no properites...
}
let myObj = MyClass()
print(heapSize(myObj)) //->16

class MyBiggerClass {
    var str: String?
    var i: Int = 0
}
let myBiggerObj = MyBiggerClass()
print(heapSize(myBiggerObj)) //->64

Seems the current Swift runtime uses malloc-compatible something to allocate memory in heap. (malloc gives some padding to fit the allocated size to power to 2, when allocating small chunks. So, the actually needed size for the instance may be smaller than the malloc_size.)

I haven't tested how far this would work, and undocumented behaviours just depending on the current implementation would change at any time in the future without any notifications.

But if you actually know that, this can be a good starting point for exploration.

Alexander
  • 59,041
  • 12
  • 98
  • 151
OOPer
  • 47,149
  • 6
  • 107
  • 142
  • Good starting point! Not ideal however, since `malloc_size` can return a bigger value than what's actually used. – Alexander Oct 30 '16 at 23:12