0

I have a MacBook Pro 15-Inch "Core i7" 2.8 Mid-2015, running Catalina 10.15.7 - detailed specs here: https://everymac.com/systems/apple/macbook_pro/specs/macbook-pro-core-i7-2.8-15-dual-graphics-mid-2015-retina-display-specs.html

My question is about the meaning of some of the variables displayed when I run sysctl -a.

My understanding is that I have 1 CPU package, 4 cores, and 8 logical cores (implemented with hyperthreading, so 2 hyperthreads per core), corresponding to these variables, which all make sense to me:

hw.packages: 1
hw.physicalcpu: 4
hw.physicalcpu_max: 4
hw.logicalcpu: 8
hw.logicalcpu_max: 8

However, I don't understand the meanings of these apparently related variables:

machdep.cpu.logical_per_package: 16
machdep.cpu.cores_per_package: 8
machdep.cpu.core_count: 4
machdep.cpu.thread_count: 8

It seems that: machdep.cpu.core_count: 4 corresponds to the number of physical cores, machdep.cpu.thread_count: 8 corresponds to the number of "hyperthreads", and machdep.cpu.cores_per_package: 8 corresponds to the number of "virtual" cores seen by the operating system. (Will these last two variables always have the same value? What is the difference between (virtual) cores per package and thread count?)

Most confusing is machdep.cpu.logical_per_package: 16. My only thought is that somehow the number of hyperthreads per core was multiplied by the number of cores per package in error.

So those are my guesses, but I'd like to know: What exactly do these variables refer to?

1 Answer 1

3

tl;dr: This is correct behavior, as designed by Intel.


Details:

machdep.cpu.logical_per_package is implemented in the kernel in bsd/dev/i386/sysctl.c:

static int
cpu_logical_per_package SYSCTL_HANDLER_ARGS
{
    __unused struct sysctl_oid *unused_oidp = oidp;
    __unused void *unused_arg1 = arg1;
    __unused int unused_arg2 = arg2;
    i386_cpu_info_t *cpu_info = cpuid_info();

    if (!(cpuid_features() & CPUID_FEATURE_HTT)) {
        return ENOENT;
    }

    return SYSCTL_OUT(req, &cpu_info->cpuid_logical_per_package,
               sizeof(cpu_info->cpuid_logical_per_package));
}

This sysctl returns the cpuid_logical_per_package field of the cpu_info structure. To see how that field is populated, we examine osfmk/i386/cpuid.c:

static void
cpuid_set_generic_info(i386_cpu_info_t *info_p)
{

...

    cpuid_fn(1, reg);

...

    if (info_p->cpuid_features & CPUID_FEATURE_HTT) {
        info_p->cpuid_logical_per_package =
            bitfield32(reg[ebx], 23, 16);
    } else {
        info_p->cpuid_logical_per_package = 1;
    }

...

}

The first part of this function executes the Intel x86 CPUID instruction with an operand value of 1, which will gather certain information from the CPU and populate the E[A,B,C,D]X registers with it. Later on, we examine bits 16-23 of the EBX register and save them into the cpuid_logical_per_package field.

To understand the meaning of what will be found in that bitfield of EBX, we turn to the Intel Software Developer's Manual, Volume 2A, Chapter 3, Page 215 (CPUID—CPU Identification), and find the register definition for the 1 opcode:

Table of CPUID operand 1 results in the E*X registers from Intel SDM Volume 2A, Chapter 3, Page 215 - CPUID

The part we're interested in says:

Bits 23 - 16: Maximum number of addressable IDs for logical processors in this physical package *.

*- The nearest power-of-2 integer that is not smaller than EBX[23:16] is the number of unique initial APIC IDs reserved for addressing different logical processors in a physical package. This field is only valid if CPUID.1.EDX.HTT[bit 28]= 1.

So what this is telling us is that the sysctl selector name "cpu_logical_per_package" is a misnomer. It tells you the valid range of ID numbers that the interrupt controller may use to identify logical CPUs and this does not necessarily correspond to how many logical CPUs you actually have.

The situation is analogous for machdep.cpu.cores_per_package, which is generated in cpuid_set_cache_info() in the same osfmk/i386/cpuid.c file:

static void
cpuid_set_cache_info( i386_cpu_info_t * info_p )
{

...

    reg[eax] = 4;           /* cpuid request 4 */
    reg[ecx] = index;       /* index starting at 0 */
    cpuid(reg);

...

    info_p->cpuid_cores_per_package
            = bitfield32(reg[eax], 31, 26) + 1;

...

}

And looking at the SDM:

Table of CPUID operand 4 results in the E*X registers from Intel SDM Volume 2A, Chapter 3, Page 216 - CPUID

So the moral is:

You should rely on machdep.cpu.core_count and .thread_count to determine the actual number of physical and logical cores, respectively, and regard the other two fields as being useful only to programmers who need to work with the kernel and have a specific need to know the maximum addressable ID for logical and physical processors.


If you're still wondering why a logical or physical processor is allowed to have a higher ID than the actual number of processors available on the silicon, the reason is that it's a result of the way that silicon is manufactured. Chip fab is an enormously expensive and complex process, and any reduction in complexity is going to save millions of dollars. It turns out to actually be cheaper to manufacture a chip with eight physical cores etched into the die and then burn a few one-time-programmable internal fuses to disconnect four of the physical cores from that circuit, than it is to create dedicated masks and tooling for a totally separate four-core die. So Intel makes a bunch of 8-core chips and then sells the amputated 4-core version for cheaper.

As far as most of the rest of the chip is concerned, though, it still thinks that it has 8 cores. Each subsystem on a processor has a unique ID by which the rest of the subsystems can identify it, and these IDs are physically implemented as arrays of transistors that build up a sequence of binary 1s and 0s to represent a decimal number. Because these IDs are made from transistors, they are immutable parts of the mask and they do not change even if the OTP fuses are blown to disable half the cores. So you may end up with an original 8-core chip having, e.g., logical core IDs ranging from 0-15 but the cheaper 4-core version only uses IDs 8-15 and the first 0-7 are not actually addressable. Hence the numbering scheme can go higher than the actual number of cores you have available to you. Hope that makes sense!

5
  • It is even weirder on my 2core MacBook 2016 machdep.cpu.logical_per_package: 16 machdep.cpu.cores_per_package: 8 machdep.cpu.core_count: 2 machdep.cpu.thread_count: 4
    – Gilby
    Commented Dec 10, 2022 at 9:35
  • @Gilby Your comment prompted me to look closer at the definition for .cores_per_package and update my answer. The reasoning for that one is analogous to that of .cpuid_logical_per_package. I added an explanation for the weirdness at the end of my updated answer.
    – pion
    Commented Dec 11, 2022 at 1:32
  • Fantastic answer. I have learnt a lot.
    – Gilby
    Commented Dec 11, 2022 at 2:51
  • Thanks, this is a great answer! I really appreciate that you systematically went through the steps for getting the information, which to a noob like me, is a process that can seem a bit overwhelming at first. Plus, you explained the deep reason behind this confusing behavior, which is something I'd want to know but never would have had the time to figure out myself. Thanks again. Commented Dec 11, 2022 at 15:15
  • I'd like to confirm: You said that "cpu_logical_per_package" tells you the valid range of ID numbers that the interrupt controller may use to identify logical CPUs", but the footnote seems to say that a separate value – the number of initial APIC IDs – is calculated from "cpu_logical_per_package", i.e., if cpu_logical_per_package is, say, 9, b1001 (not realistic, but for illustration), then the number of APIC IDs would have to be represented by at least 4 bits, giving 2^4 (16) values, which would be the default ("initial") number of APIC IDs, which I assume the programmer could reduce to 9. Commented Dec 11, 2022 at 17:01

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .