274

曾几何时,要编写 x86 汇编程序,例如,您会看到说明“将 EDX 寄存器加载为 5”、“增加 EDX 寄存器”等指令。

对于具有 4 个内核(甚至更多内核)的现代 CPU,在机器代码级别是否看起来像有 4 个独立的 CPU(即只有 4 个不同的“EDX”寄存器)?如果是这样,当您说“增加 EDX 寄存器”时,是什么决定了哪个 CPU 的 EDX 寄存器增加?现在 x86 汇编器中是否有“CPU 上下文”或“线程”概念?

核心之间的通信/同步如何工作?

如果您正在编写一个操作系统,通过硬件公开什么机制来允许您在不同的内核上调度执行?它是一些特殊的特权指令吗?

如果您正在为多核 CPU 编写优化编译器/字节码 VM,您需要特别了解 x86 以使其生成可在所有内核上高效运行的代码?

对 x86 机器代码进行了哪些更改以支持多核功能?

4

11 回答 11

182

这不是对问题的直接回答,而是对评论中出现的问题的回答。本质上,问题是硬件为多线程操作提供了什么支持。

Nicholas Flynt 是对的,至少在 x86 方面是这样。在多线程环境(超线程、多核或多处理器)中,Bootstrap 线程(通常是处理器 0 中的内核 0 中的线程 0)启动从 address 获取代码0xfffffff0所有其他线程都以称为Wait-for-SIPI的特殊睡眠状态启动。作为其初始化的一部分,主线程通过 APIC 向 WFS 中的每个线程发送一个称为 SIPI (Startup IPI) 的特殊处理器间中断 (IPI)。SIPI 包含该线程应该开始获取代码的地址。

这种机制允许每个线程从不同的地址执行代码。所需要的只是为每个线程建立自己的表和消息队列的软件支持。操作系统使用这些来执行实际的多线程调度。

就实际程序集而言,正如 Nicholas 所写,单线程或多线程应用程序的程序集之间没有区别。每个逻辑线程都有自己的寄存器集,因此编写:

mov edx, 0

只会更新EDX当前正在运行的线程。无法EDX使用单个汇编指令在另一个处理器上进行修改。您需要某种系统调用来要求操作系统告诉另一个线程运行将更新它自己的代码EDX

于 2009-06-13T18:09:01.217 回答
121

Intel x86 最小可运行裸机示例

具有所有必需样板的可运行裸机示例。下面介绍了所有主要部分。

在 Ubuntu 15.10 QEMU 2.3.0 和 Lenovo ThinkPad T400真实硬件客户机上测试。

英特尔手册第 3 卷系统编程指南 - 325384-056US 2015 年 9 月在第 8、9 和 10 章中介绍了 SMP。

表 8-1。“广播 INIT-SIPI-SIPI 序列和超时选择”包含一个基本上可以正常工作的示例:

MOV ESI, ICR_LOW    ; Load address of ICR low dword into ESI.
MOV EAX, 000C4500H  ; Load ICR encoding for broadcast INIT IPI
                    ; to all APs into EAX.
MOV [ESI], EAX      ; Broadcast INIT IPI to all APs
; 10-millisecond delay loop.
MOV EAX, 000C46XXH  ; Load ICR encoding for broadcast SIPI IP
                    ; to all APs into EAX, where xx is the vector computed in step 10.
MOV [ESI], EAX      ; Broadcast SIPI IPI to all APs
; 200-microsecond delay loop
MOV [ESI], EAX      ; Broadcast second SIPI IPI to all APs
                    ; Waits for the timer interrupt until the timer expires

在那个代码上:

  1. 大多数操作系统将使大多数这些操作无法通过环 3(用户程序)进行。

    因此,您需要编写自己的内核来自由地使用它:用户态 Linux 程序将无法运行。

  2. 首先,运行一个称为引导处理器 (BSP) 的处理器。

    它必须通过称为处理器间中断 (IPI)的特殊中断唤醒其他处理器(称为应用处理器 (AP) ) 。

    这些中断可以通过中断命令寄存器 (ICR) 对高级可编程中断控制器 (APIC) 进行编程来完成

    ICR 的格式记录在:10.6 “发出 INTERPROCESSOR INTERRUPTS”

    IPI 在我们写入 ICR 后立即发生。

  3. ICR_LOW 在 8.4.4“MP 初始化示例”中定义为:

    ICR_LOW EQU 0FEE00300H
    

    魔术值0FEE00300是 ICR 的内存地址,如表 10-1“本地 APIC 寄存器地址映射”中所述

  4. 示例中使用了最简单的方法:它设置 ICR 以发送广播 IPI,这些 IPI 将被传递到除当前处理器之外的所有其他处理器。

    但是也有可能并且被一些人推荐,通过 BIOS 设置的特殊数据结构(如ACPI 表或 Intel 的 MP 配置表)获取有关处理器的信息,并且只唤醒您需要的那些。

  5. XXin000C46XXH将处理器将执行的第一条指令的地址编码为:

    CS = XX * 0x100
    IP = 0
    

    请记住,CS 将地址乘以0x10,因此第一条指令的实际内存地址为:

    XX * 0x1000
    

    因此,例如XX == 1,处理器将从0x1000.

    然后,我们必须确保在该内存位置运行 16 位实模式代码,例如:

    cld
    mov $init_len, %ecx
    mov $init, %esi
    mov 0x1000, %edi
    rep movsb
    
    .code16
    init:
        xor %ax, %ax
        mov %ax, %ds
        /* Do stuff. */
        hlt
    .equ init_len, . - init
    

    使用链接描述文件是另一种可能性。

  6. 延迟循环是一个令人讨厌的部分:没有超级简单的方法可以精确地进行这种睡眠。

    可能的方法包括:

    • PIT(在我的示例中使用)
    • 高能量PET
    • 使用上述校准繁忙循环的时间,并改用它

    相关:如何使用 DOS x86 程序集在屏幕上显示数字并休眠一秒钟?

  7. 我认为初始处理器需要处于保护模式才能工作,因为我们写入的地址0FEE00300H对于 16 位来说太高了

  8. 为了在处理器之间进行通信,我们可以在主进程上使用自旋锁,并从第二个核心修改锁。

    我们应该确保完成内存写回,例如通过wbinvd.

处理器之间的共享状态

8.7.1“逻辑处理器的状态”说:

以下功能是支持英特尔超线程技术的英特尔 64 或 IA-32 处理器中逻辑处理器架构状态的一部分。这些功能可以细分为三组:

  • 为每个逻辑处理器复制
  • 由物理处理器中的逻辑处理器共享
  • 共享或复制,取决于实现

每个逻辑处理器都有以下功能:

  • 通用寄存器(EAX、EBX、ECX、EDX、ESI、EDI、ESP 和 EBP)
  • 段寄存器(CS、DS、SS、ES、FS 和 GS)
  • EFLAGS 和 EIP 寄存器。请注意,每个逻辑处理器的 CS 和 EIP/RIP 寄存器指向逻辑处理器正在执行的线程的指令流。
  • x87 FPU 寄存器(ST0 到 ST7、状态字、控制字、标签字、数据操作数指针和指令指针)
  • MMX 寄存器(MM0 到 MM7)
  • XMM 寄存器(XMM0 到 XMM7)和 MXCSR 寄存器
  • 控制寄存器和系统表指针寄存器(GDTR、LDTR、IDTR、任务寄存器)
  • 调试寄存器(DR0、DR1、DR2、DR3、DR6、DR7)和调试控制 MSR
  • 机器检查全局状态 (IA32_MCG_STATUS) 和机器检查能力 (IA32_MCG_CAP) MSR
  • 热时钟调制和 ACPI 电源管理控制 MSR
  • 时间戳计数器 MSR
  • 大多数其他 MSR 寄存器,包括页属性表 (PAT)。请参阅下面的例外情况。
  • 本地 APIC 寄存器。
  • 其他通用寄存器 (R8-R15)、XMM 寄存器 (XMM8-XMM15)、控制寄存器、Intel 64 处理器上的 IA32_EFER。

以下功能由逻辑处理器共享:

  • 内存类型范围寄存器 (MTRR)

以下功能是共享还是复制是特定于实现的:

  • IA32_MISC_ENABLE MSR(MSR 地址 1A0H)
  • 机器检查架构 (MCA) MSR(IA32_MCG_STATUS 和 IA32_MCG_CAP MSR 除外)
  • 性能监控控制和计数器 MSR

缓存共享在以下位置讨论:

英特尔超线程比单独的内核具有更大的缓存和管道共享:https ://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Linux 内核 4.2

主要的初始化动作似乎在arch/x86/kernel/smpboot.c.

ARM 最小可运行裸机示例

在这里,我为 QEMU 提供了一个最小的可运行 ARMv8 aarch64 示例:

.global mystart
mystart:
    /* Reset spinlock. */
    mov x0, #0
    ldr x1, =spinlock
    str x0, [x1]

    /* Read cpu id into x1.
     * TODO: cores beyond 4th?
     * Mnemonic: Main Processor ID Register
     */
    mrs x1, mpidr_el1
    ands x1, x1, 3
    beq cpu0_only
cpu1_only:
    /* Only CPU 1 reaches this point and sets the spinlock. */
    mov x0, 1
    ldr x1, =spinlock
    str x0, [x1]
    /* Ensure that CPU 0 sees the write right now.
     * Optional, but could save some useless CPU 1 loops.
     */
    dmb sy
    /* Wake up CPU 0 if it is sleeping on wfe.
     * Optional, but could save power on a real system.
     */
    sev
cpu1_sleep_forever:
    /* Hint CPU 1 to enter low power mode.
     * Optional, but could save power on a real system.
     */
    wfe
    b cpu1_sleep_forever
cpu0_only:
    /* Only CPU 0 reaches this point. */

    /* Wake up CPU 1 from initial sleep!
     * See:https://github.com/cirosantilli/linux-kernel-module-cheat#psci
     */
    /* PCSI function identifier: CPU_ON. */
    ldr w0, =0xc4000003
    /* Argument 1: target_cpu */
    mov x1, 1
    /* Argument 2: entry_point_address */
    ldr x2, =cpu1_only
    /* Argument 3: context_id */
    mov x3, 0
    /* Unused hvc args: the Linux kernel zeroes them,
     * but I don't think it is required.
     */
    hvc 0

spinlock_start:
    ldr x0, spinlock
    /* Hint CPU 0 to enter low power mode. */
    wfe
    cbz x0, spinlock_start

    /* Semihost exit. */
    mov x1, 0x26
    movk x1, 2, lsl 16
    str x1, [sp, 0]
    mov x0, 0
    str x0, [sp, 8]
    mov x1, sp
    mov w0, 0x18
    hlt 0xf000

spinlock:
    .skip 8

GitHub 上游.

组装并运行:

aarch64-linux-gnu-gcc \
  -mcpu=cortex-a57 \
  -nostdlib \
  -nostartfiles \
  -Wl,--section-start=.text=0x40000000 \
  -Wl,-N \
  -o aarch64.elf \
  -T link.ld \
  aarch64.S \
;
qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a57 \
  -d in_asm \
  -kernel aarch64.elf \
  -nographic \
  -semihosting \
  -smp 2 \
;

在本例中,我们将 CPU 0 置于自旋锁循环中,它仅在 CPU 1 释放自旋锁时退出。

在自旋锁之后,CPU 0 执行一个半主机退出调用,使 QEMU 退出。

如果你只用一个 CPU 启动 QEMU -smp 1,那么模拟就永远挂在自旋锁上。

CPU 1 被 PSCI 接口唤醒,更多细节在:ARM: Start/Wakeup/Bringup the other CPU cores/APs and pass execution start address?

上游版本还进行了一些调整以使其适用于 gem5,因此您也可以尝试性能特征。

我还没有在真正的硬件上测试过它,所以我不确定它的便携性。以下 Raspberry Pi 参考书目可能会引起您的兴趣:

本文档提供了一些关于使用 ARM 同步原语的指导,您可以使用这些原语来做有趣的多核操作:http: //infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

在 Ubuntu 18.10、GCC 8.2.0、Binutils 2.31.1、QEMU 2.12.0 上测试。

更方便的可编程性的后续步骤

前面的示例唤醒辅助 CPU 并使用专用指令进行基本的内存同步,这是一个好的开始。

但是为了使多核系统易于编程,例如POSIX pthreads,您还需要进入以下更多涉及的主题:

  • 设置中断并运行一个计时器,该计时器定期决定现在将运行哪个线程。这称为抢占式多线程

    这样的系统还需要在线程寄存器启动和停止时保存和恢复它们。

    也有可能拥有非抢占式多任务系统,但这些系统可能需要您修改代码以使每个线程都能产生(例如,通过pthread_yield实现),并且平衡工作负载变得更加困难。

    以下是一些简单的裸机计时器示例:

  • 处理内存冲突。值得注意的是,如果您想用 C 或其他高级语言进行编码,每个线程都需要一个唯一的堆栈。

    您可以将线程限制为具有固定的最大堆栈大小,但处理此问题的更好方法是使用允许有效的“无限大小”堆栈的分页。

    这是一个天真的 aarch64 裸机示例,如果堆栈增长得太深,它会爆炸

这些是使用 Linux 内核或其他操作系统的一些很好的理由 :-)

用户态内存同步原语

尽管线程启动/停止/管理通常超出用户空间范围,但是您可以使用来自用户空间线程的汇编指令来同步内存访问,而无需潜在的更昂贵的系统调用。

您当然应该更喜欢使用可移植地包装这些低级原语的库。<mutex>C++ 标准本身在和标头上取得了很大进步<atomic>,尤其是std::memory_order. 我不确定它是否涵盖了所有可能实现的内存语义,但它只是可能。

更微妙的语义在无锁数据结构的上下文中特别相关,在某些情况下可以提供性能优势。要实现这些,您可能需要了解不同类型的内存屏障:https ://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

例如,Boost 在以下位置有一些无锁容器实现:https ://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

此类用户态指令似乎也用于实现 Linuxfutex系统调用,这是 Linux 中的主要同步原语之一。man futex4.15 内容如下:

futex() 系统调用提供了一种等待某个条件成立的方法。它通常用作共享内存同步上下文中的阻塞构造。使用 futex 时,大多数同步操作都是在用户空间中执行的。用户空间程序仅在程序可能必须阻塞更长的时间直到条件变为真时才使用 futex() 系统调用。其他 futex() 操作可用于唤醒任何等待特定条件的进程或线程。

系统调用名称本身的意思是“快速用户空间 XXX”。

这是一个带有内联汇编的最小无用 C++ x86_64 / aarch64 示例,它说明了此类指令的基本用法,主要是为了好玩:

主文件

#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>

std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
#if defined(__x86_64__) || defined(__aarch64__)
unsigned long my_arch_atomic_ulong = 0;
unsigned long my_arch_non_atomic_ulong = 0;
#endif
size_t niters;

void threadMain() {
    for (size_t i = 0; i < niters; ++i) {
        my_atomic_ulong++;
        my_non_atomic_ulong++;
#if defined(__x86_64__)
        __asm__ __volatile__ (
            "incq %0;"
            : "+m" (my_arch_non_atomic_ulong)
            :
            :
        );
        // https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
        __asm__ __volatile__ (
            "lock;"
            "incq %0;"
            : "+m" (my_arch_atomic_ulong)
            :
            :
        );
#elif defined(__aarch64__)
        __asm__ __volatile__ (
            "add %0, %0, 1;"
            : "+r" (my_arch_non_atomic_ulong)
            :
            :
        );
        // https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
        __asm__ __volatile__ (
            "ldadd %[inc], xzr, [%[addr]];"
            : "=m" (my_arch_atomic_ulong)
            : [inc] "r" (1),
              [addr] "r" (&my_arch_atomic_ulong)
            :
        );
#endif
    }
}

int main(int argc, char **argv) {
    size_t nthreads;
    if (argc > 1) {
        nthreads = std::stoull(argv[1], NULL, 0);
    } else {
        nthreads = 2;
    }
    if (argc > 2) {
        niters = std::stoull(argv[2], NULL, 0);
    } else {
        niters = 10000;
    }
    std::vector<std::thread> threads(nthreads);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i] = std::thread(threadMain);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i].join();
    assert(my_atomic_ulong.load() == nthreads * niters);
    // We can also use the atomics direclty through `operator T` conversion.
    assert(my_atomic_ulong == my_atomic_ulong.load());
    std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
#if defined(__x86_64__) || defined(__aarch64__)
    assert(my_arch_atomic_ulong == nthreads * niters);
    std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
#endif
}

GitHub 上游.

可能的输出:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

从这里我们看到 x86 LOCK 前缀/aarch64LDADD指令使加法成为原子:没有它,我们在许多加法上都有竞争条件,最后的总计数小于同步的 20000。

也可以看看:

在 Ubuntu 19.04 amd64 和 QEMU aarch64 用户模式下测试。

于 2015-11-11T13:22:11.783 回答
47

据我了解,每个“核心”都是一个完整的处理器,有自己的寄存器集。基本上,BIOS 以一个内核运行开始,然后操作系统可以通过初始化其他内核并将它们指向要运行的代码等来“启动”其他内核。

同步由操作系统完成。通常,每个处理器为操作系统运行不同的进程,因此操作系统的多线程功能负责决定哪个进程访问哪个内存,以及在内存冲突的情况下该怎么做。

于 2009-06-11T13:21:22.143 回答
42

非官方 SMP 常见问题解答 堆栈溢出徽标


曾几何时,要编写 x86 汇编程序,例如,您会收到说明“将 EDX 寄存器的值加载为 5”、“增加 EDX”寄存器等指令。使用具有 4 个内核(甚至更多)的现代 CPU ,在机器代码级别,它只是看起来像有 4 个独立的 CPU(即只有 4 个不同的“EDX”寄存器)吗?

确切地。有 4 组寄存器,包括 4 个独立的指令指针。

如果是这样,当您说“增加 EDX 寄存器”时,是什么决定了哪个 CPU 的 EDX 寄存器增加?

自然是执行该指令的 CPU。可以将其视为 4 个完全不同的微处理器,它们只是共享相同的内存。

现在 x86 汇编器中是否有“CPU 上下文”或“线程”概念?

不,汇编器只是像往常一样翻译指令。那里没有变化。

核心之间的通信/同步如何工作?

由于它们共享相同的内存,这主要是程序逻辑的问题。虽然现在有一个处理器间中断机制,但它不是必需的,并且最初不存在于第一个双 CPU x86 系统中。

如果您正在编写一个操作系统,通过硬件公开什么机制来允许您在不同的内核上调度执行?

调度程序实际上并没有改变,只是它对临界区和使用的锁类型更加小心。在 SMP 之前,内核代码最终会调用调度程序,调度程序会查看运行队列并选择一个进程作为下一个线程运行。(内核的进程看起来很像线程。)SMP 内核运行完全相同的代码,一次一个线程,只是现在关键部分锁定需要 SMP 安全,以确保两个内核不会意外选择相同的PID。

它是一些特殊的特权指令吗?

不,所有内核都使用相同的旧指令在相同的内存中运行。

如果您正在为多核 CPU 编写优化编译器/字节码 VM,您需要特别了解 x86 以使其生成可在所有内核上高效运行的代码?

您运行与以前相同的代码。需要更改的是 Unix 或 Windows 内核。

您可以将我的问题总结为“对 x86 机器代码进行了哪些更改以支持多核功能?”

没有什么是必要的。第一个 SMP 系统使用与单处理器完全相同的指令集。现在,已经有大量的 x86 架构演变和数以万计的新指令使事情变得更快,但对于 SMP 来说,这些都不是必需的。

有关详细信息,请参阅英特尔多处理器规范


更新:只要完全接受n路多核 CPU与n 个共享相同内存的独立处理器几乎完全相同,就可以回答所有后续问题。2 有一个重要的问题没有被问到:如何编写一个程序以在多个内核上运行以获得更高的性能?答案是:它是使用像Pthreads 这样的线程库编写的。一些线程库使用操作系统不可见的“绿色线程”,它们不会获得单独的内核,但只要线程库使用内核线程功能,那么您的线程程序将自动成为多核。
1. 为了向后兼容,只有第一个核心在复位时启动,并且需要做一些驱动类型的事情来启动其余的。
2. 他们自然也共享所有外围设备。

于 2013-02-07T21:22:18.650 回答
11

如果您正在为多核 CPU 编写优化编译器/字节码 VM,您需要特别了解 x86 以使其生成可在所有内核上高效运行的代码?

作为编写优化编译器/字节码虚拟机的人,我可以在这里为您提供帮助。

您无需了解任何有关 x86 的具体知识,即可生成可在所有内核上高效运行的代码。

但是,您可能需要了解 cmpxchg 和朋友,才能编写在所有内核上正确运行的代码。多核编程需要在执行线程之间使用同步和通信。

您可能需要了解一些关于 x86 的知识,以使其生成通常在 x86 上高效运行的代码。

还有其他一些对你有用的东西:

您应该了解操作系统(Linux 或 Windows 或 OSX)提供的允许您运行多个线程的设施。您应该了解并行化 API,例如 OpenMP 和 Threading Building Blocks,或 OSX 10.6 “Snow Leopard”即将推出的“Grand Central”。

您应该考虑您的编译器是否应该自动并行化,或者由您的编译器编译的应用程序的作者是否需要在他的程序中添加特殊语法或 API 调用以利用多个内核。

于 2009-06-11T13:49:02.810 回答
9

每个核心从不同的内存区域执行。您的操作系统会将内核指向您的程序,然后内核将执行您的程序。您的程序不会知道有多个内核或它正在执行哪个内核。

也没有仅适用于操作系统的附加指令。这些内核与单核芯片相同。每个核心运行操作系统的一部分,它将处理与用于信息交换的公共内存区域的通信,以找到下一个要执行的内存区域。

这是一个简化,但它为您提供了如何完成的基本概念。Embedded.com 上有关多核和多处理器的更多 信息有很多关于这个主题的信息......这个主题很快就会变得复杂!

于 2009-06-11T13:36:39.233 回答
5

汇编代码将转换为将在一个内核上执行的机器代码。如果您希望它是多线程的,您将不得不使用操作系统原语在不同的处理器上多次启动此代码,或者在不同的内核上启动不同的代码 - 每个内核将执行一个单独的线程。每个线程只会看到它当前正在执行的一个内核。

于 2009-06-11T13:21:19.990 回答
3

它根本不是在机器指令中完成的。这些内核假装是不同的 CPU,并且没有任何特殊的相互交谈的能力。他们有两种沟通方式:

  • 它们共享物理地址空间。硬件处理缓存一致性,因此一个 CPU 写入另一个 CPU 读取的内存地址。

  • 它们共享一个 APIC(可编程中断控制器)。这是映射到物理地址空间的内存,一个处理器可以使用它来控制其他处理器,打开或关闭它们,发送中断等。

http://www.cheesecake.org/sac/smp.html是一个很好的参考,带有一个愚蠢的 url。

于 2009-10-27T13:56:55.950 回答
1

单线程和多线程应用程序的主要区别在于前者有一个堆栈,而后者每个线程都有一个堆栈。由于编译器将假定数据和堆栈段寄存器(ds 和 ss)不相等,因此生成的代码略有不同。这意味着通过默认为 ss 寄存器的 ebp 和 esp 寄存器的间接寻址不会也默认为 ds(因为 ds!=ss)。相反,通过默认为 ds 的其他寄存器的间接寻址不会默认为 ss。

线程共享其他所有内容,包括数据和代码区域。它们还共享 lib 例程,因此请确保它们是线程安全的。对 RAM 中的区域进行排序的过程可以是多线程的以加快速度。然后,线程将访问、比较和排序同一物理内存区域中的数据,并执行相同的代码,但使用不同的局部变量来控制它们各自的排序部分。这当然是因为线程有不同的堆栈,其中包含局部变量。这种类型的编程需要仔细调整代码,以减少内核间数据冲突(在高速缓存和 RAM 中),这反过来导致使用两个或更多线程的代码比仅使用一个线程更快。当然,未经调整的代码使用一个处理器通常会比使用两个或更多处理器更快。调试更具挑战性,因为标准的“int 3”断点将不适用,因为您想中断特定线程而不是所有线程。调试寄存器断点也不能解决这个问题,除非您可以在执行您要中断的特定线程的特定处理器上设置它们。

其他多线程代码可能涉及在程序的不同部分运行的不同线程。这种类型的编程不需要相同类型的调整,因此更容易学习。

于 2011-02-21T23:18:40.770 回答
1

我认为提问者可能希望通过让多个内核并行工作来使程序运行得更快。无论如何,这就是我想要的,但所有的答案都让我不明智。但是,我想我明白了:您无法将不同的线程同步到指令执行时间的准确性。因此,您无法让 4 个内核并行地对四个不同的数组元素进行乘法运算以将处理速度提高 4:1。相反,您必须将程序视为包含按顺序执行的主要块,例如

  1. 对某些数据进行 FFT
  2. 将结果放入矩阵中,求其特征值和特征向量
  3. 后者按特征值排序
  4. 使用新数据从第一步开始重复

您可以做的是对第 1 步的结果运行第 2 步,同时在不同核心中对新数据运行第 1 步,并在第 2 步对下一个数据和步骤运行时在不同核心中对第 2 步的结果运行第 3 步1 之后在数据上运行。您可以在 Compaq Visual Fortran 和 Intel Fortran 中执行此操作,这是 CVF 的演变,通过为三个步骤编写三个单独的程序/子例程,而不是一个“调用”下一个它调用 API 来启动其线程。他们可以通过使用 COMMON 共享数据,这将是所有线程的 COMMON 数据内存。您必须研究手册直到头疼并进行试验,直到使它起作用,但我至少成功了一次。

于 2021-12-10T22:39:30.517 回答
0

与之前的单处理器变体相比,每个支持多处理的架构都添加了在内核之间同步的指令。此外,您还有处理缓存一致性、刷新缓冲区和操作系统必须处理的类似低级操作的指令。在 IBM POWER6、IBM Cell、Sun Niagara 和英特尔“超线程”等同时多线程架构的情况下,您还倾向于看到新指令来确定线程之间的优先级(例如设置优先级并在无事可做时显式让出处理器) .

但是基本的单线程语义是相同的,您只需添加额外的工具来处理与其他内核的同步和通信。

于 2009-08-18T18:20:46.973 回答