2025-09-30 05:28:33
在软件中,我们使用二进制浮点数来表示实数。实际上,我们用一个固定精度的整数(有效数字)乘以二的幂来表示实数。因此,我们不能精确表示数字“pi”,但可以得到一个非常接近的近似值:3.141592653589793115997963468544185161590576171875。前16位数字是精确的。
你可以将pi的近似值表示为7074237752028440乘以2的-51次方。用这种方式表示数字有点烦人,因此我们可以使用十六进制浮点数表示法。
在十六进制浮点数表示法中,数字1表示为0x1.0,数字2表示为0x2.0。它的工作方式与十进制数字类似,只不过一半的值0.8表示为0x0.8,而无限长的字符串0xffffff…则表示为1。
让我们回到pi这个数字。我们将有效数字表示为十六进制数,即0x1921fb54442d18,并在第一个数字后面插入小数点,得到0x1.921fb54442d18。这是一个在[1,2)范围内的数字,具体是0x1921fb54442d18乘以2的-52次方。
为了得到pi,我们需要乘以2,这可以通过在末尾添加“p+1”来实现:0x1.921fb544442d18p+1。
当然,你不会手动计算这些。在现代C++中,只需执行以下操作:
std::print("Pi (hex): {:a}\n", std::numbers::pi);
如果你使用的是64位浮点数,那么你可以表示的范围大约是从-1.7976931348623157e+308到1.7976931348623157e308,使用指数表示法,其中1.79769e308表示为1.79769乘以10的308次方。在十六进制表示法中,最大值是0x1.fffffffffffffp+1023。整数0x1fffffffffffff等于9007199254740991,最大值是9007199254740991乘以2的1023-52次方。
超出此范围的数字会被表示为无穷大值。事实上,我们的计算机有无穷大的概念,并且知道1除以无穷大等于0。因此,在现代C++中,以下代码将打印出零:
double infinity = std::numeric_limits
2025-09-22 03:10:18
当我还是本科生的时候,我发现了符号代数。这太棒了!我不再需要手动解变量,只需将所有方程输入计算机就能得到结果。 不久后我意识到,符号代数并没有让我成为天才数学家。它能处理繁琐的计算,但我也常常发现自己“卡住”了。我从一个问题开始,将它输入机器,却得到一堆毫无头绪的结果,离答案反而更远。 随着时间推移,我逐渐认识到这些工具对我产生了不良影响。要知道,人类天性懒惰。如果能够避免深入思考某个问题,我们自然会这么做。 我担心,这种情况在大型语言模型身上也正在发生。为什么还要仔细思考?为什么还要阅读文档?不如让机器不断尝试,直到成功为止。 这有效。符号代数可以解决大量有趣的数学问题,而大型语言模型则能解决更多问题。 但如果你只是盲目地依赖大型语言模型,你的新技能又从何而来?你的深刻洞察又从何而来? 我不是卢德主义者。我鼓励大家在可以使用新工具时拥抱它们。但你们不应该放弃做艰苦的工作。
2025-09-08 03:44:56
假设你有一个很长的字符串,并且想要每72个字符插入一个换行符。你可能需要这样做,如果你需要将公钥写入文本文件。 一个简单的C函数应该就足够了。我使用字母K来表示行的长度。我从输入缓冲区复制到输出缓冲区。 void insert_line_feed(const char *buffer, size_t length, int K, char *output) { if (K == 0) { memcpy(output, buffer, length); return; } size_t input_pos = 0; size_t next_line_feed = K; while (input_pos 0x80的比较来确定插入点,并将结果与包含换行符的向量混合以实现高效的并行处理。 inline __m256i insert_line_feed32(__m256i input, int N) { __m256i line_feed_vector = _mm256_set1_epi8('\n'); __m128i identity = _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); if (K >= 16) { __m128i maskhi = _mm_loadu_si128(shuffle_masks[N - 16]); __m256i mask = _mm256_set_m128i(maskhi, identity); __m256i lf_pos = _mm256_cmpeq_epi8(mask, _mm256_set1_epi8(0x80)); __m256i shuffled = _mm256_shuffle_epi8(input, mask); __m256i result = _mm256_blendv_epi8(shuffled, line_feed_vector, lf_pos); return result; } // 右移输入一位字节 __m256i shift = _mm256_alignr_epi8( input, _mm256_permute2x128_si256(input, input, 0x21), 15); input = _mm256_blend_epi32(input, shift, 0xF0); __m128i masklo = _mm_loadu_si128(shuffle_masks[N]); __m256i mask = _mm256_set_m128i(identity, masklo); __m256i lf_pos = _mm256_cmpeq_epi8(mask, _mm256_set1_epi8(0x80)); __m256i shuffled = _mm256_shuffle_epi8(input, mask); __m256i result = _mm256_blendv_epi8(shuffled, line_feed_vector, lf_pos); return result; } 通过使用这样的高级函数,我们能更快吗?让我们测试一下。我编写了一个基准测试。我在Intel Ice Lake处理器上使用GCC 12进行大输入字符串的测试。 逐字符处理 1.0 GB/s 8.0 指令/字节 memcpy 11 GB/s 0.46 指令/字节 AVX2 16 GB/s 0.52 指令/字节
2025-09-01 22:26:18
我们的处理器基于时钟执行指令。因此,4 GHz处理器每秒有40亿个周期。增加我们的处理器时钟频率是困难的。如果你超过5 GHz太多,处理器可能会过热或以其他方式失效。 那么,我们如何让处理器更快?现代处理器可以同时执行多条指令。这有时被称为超标量执行。大多数处理器每个周期可以处理4条指令或更多。最近的苹果处理器每个周期可以轻松执行超过8条指令。 然而,执行的指令数量取决于指令本身。有些指令成本较低(如加法),有些则成本较高(如整数除法)。指令成本越低,每个周期可以执行的指令就越多。 处理器有多个执行单元。如果有四个执行单元可以执行加法,你可能每个周期执行4次加法。更多的执行单元允许每个周期执行更多指令。 通常,x86-64处理器(Intel/AMD)每个周期最多只能完成一条乘法指令,使得乘法相比加法成本较高。相比之下,最近的苹果处理器每个周期可以完成两条乘法指令。 最新的AMD处理器(Zen 5)有三个可以执行乘法的执行单元,某些情况下可能每个周期完成3次乘法。仅从执行单元来看,Zen 5处理器理论上每个周期可以完成3条加法指令和3条乘法指令。 但这并不是全部。我只计算了通用64位寄存器上的常规乘法指令。Zen 5处理器还有四个用于512位寄存器的执行单元,其中两个可以执行乘法。这些512位寄存器允许我们通过在每个寄存器中打包多个值,同时执行大量乘法操作。 通常我们的通用处理器正在变得更宽:它们每个周期可以完成更多指令。这不是唯一可能的设计。事实上,这些更宽的处理器需要更多的晶体管。相反,你可以使用这些晶体管来制造更多的处理器。而这正是许多人所期待的:他们期待我们的计算机包含更多通用处理器。 像AMD Zen 5这样的处理器设计确实令人印象深刻。这不仅仅是增加执行单元的问题。你必须将数据传输到这些单元,安排计算,处理分支。 对程序员而言,这意味着即使你不显式使用并行性,无论做什么,你的代码都会在后台以并行方式执行。
2025-08-25 03:51:49
我最喜欢的文本编辑器是 Visual Studio Code。我认为它可能是最流行的软件开发环境。许多大型软件公司都采用了 Visual Studio Code。 命名有点奇怪,因为 Visual Studio Code 几乎与 Visual Studio 毫无关系,除了它来自微软。简而言之,我们通常称它为“VS Code”,尽管我更倾向于拼写全称。 Visual Studio Code 有一个有趣的架构。它主要使用 TypeScript(本质上是 JavaScript)基于 Electron 进行编写。Electron 本身由 Node.js 运行时环境和 Web 引擎 Chromium 组成。Electron 提供了一种使用 JavaScript 或 TypeScript 构建桌面应用程序的通用方法。 在 Electron 中,Node.js 本身基于 Google 的 v8 引擎用于 JavaScript 执行。因此,微软同时依赖于一个社区支持的引擎(Node.js)以及 Google 软件栈。 值得注意的是,尽管 Visual Studio Code 运行在 JavaScript 引擎上,它通常运行得相当快。在我的主笔记本电脑上,它启动时间约为 0.1 秒。我很少注意到任何延迟。无论是在 macOS 还是 Windows 上使用,它几乎总是响应迅速。在 Windows 上,Visual Studio Code 的感觉比 Visual Studio 更快。然而,Visual Studio 是用 C# 和 C++ 编写的,这些语言在理论上允许更好的优化。其成功之处在于 v8、Chromium 和 Node.js 的优化工作。 Visual Studio Code 似乎几乎可以无限扩展。它高度便携,并且对终端有很好的支持。结合 Microsoft Copilot,你可以在需要进行一些 vibe 编码时获得不错的 AI 体验。我也喜欢“Remote SSH”扩展,它允许你通过 ssh 连接到远程服务器,并像在本地机器上一样工作。 当我进行系统编程时,我通常使用 C 或 C++,并以 CMake 作为构建系统。在我看来,CMake 是一个出色的构建系统。我将其与 CPM 结合使用,以处理我的依赖项。 微软为 CMake 用户提供了一个有用的扩展,称为 CMake Tools。我很少使用它,但在我需要启动调试器进行非平凡工作时,它非常方便。 在大多数情况下,我在 Linux 上的调试使用很简单: 打开包含 CMake 项目的仓库。它可能会提示我选择编译器,但我不会指定。它似乎运行良好。 我通过 typing F1 并选择“CMake: Set Builder Target”来选择要运行/调试的目标。如果我只输入目标名称(例如可执行文件),它似乎也能正常工作。 在文本编辑器中,我点击想要调试器停止的行左侧。你也可以添加条件断点。 我点击 Visual Studio Code 窗口底部工具栏中的小“bug”图标。 它通常能正常工作。 对于某些项目,你可能需要向 CMake 传递一些配置标志。你只需在 .vscode 子目录中创建一个名为 settings.json 的 JSON 文件。该 JSON 文件包含一个 JSON 对象,你只需添加 cmake.configureArgs 与特殊设置,例如… {"cmake.configureArgs": ["-DSIMDJSON_DEVELOPER_MODE=ON"]} settings.json 还有其他用途。你可以设置用户界面偏好,可以排除文件以避免搜索工具查找,可以配置代码检查等。你也可以将 settings.json 文件纳入版本控制,以确保所有用户都获得相同的偏好。 不幸的是,在 macOS 上我的调试体验并不如在 Linux 上顺畅。问题可能来自于 macOS 默认使用 LLVM 而不是 GCC 作为 C 和 C++ 编译器。 因此,在 macOS 上我需要添加两个不太明显的步骤。 我安装一个名为 CodeLLDB 的扩展,由 Vadim Chugunov 开发。 我在 .vscode 子目录中创建一个名为 launch.json 的文件: {"configurations": [ { "name": "Launch (lldb)", "type": "lldb", "request": "launch", "program": "${command:cmake.launchTargetPath}", "cwd": "${workspaceFolder}", }] } 在 Visual Studio Code 窗口底部应该会出现一个“Launch (lldb)”按钮。点击它应该启动调试器。其余操作则与我在 Linux 上的使用方式相同。它通常能正常工作。 Visual Studio Code 是一个工具的实例,它从不完全完美地执行任何操作。它期望你手动编辑 JSON 文件。它期望你自行找到合适的扩展。调试环境本身不错,但你不会为之写情书。但整个 Visual Studio Code 套件却成功地表现出色。所有东西似乎都足够好,让你能以最少的麻烦完成工作。 网络本身依赖于通用技术(HTML、CSS、JavaScript),虽然每种技术都有其不完美之处,但它们形成了一种协调且可适应的整体。Visual Studio Code 体现了这种哲学:它并不试图完美地完成所有事情,而是提供了一个平台,让每位开发者都能构建自己的工作流程。这种模块化,结合简洁的界面和活跃的社区,解释了为什么它成为了我最喜欢的工具之一。
2025-08-16 05:42:56

从内存加载数据通常需要几个纳秒。当处理器等待数据时,可能会被迫等待而无法执行有用的工作。现代处理器中的硬件预取器通过在数据被请求前将其加载到缓存中,从而优化性能。其效果取决于访问模式:顺序读取能从高效的预取中受益,而随机访问则不能。
为了测试预取器的影响,我编写了一个Go程序,使用单一数组访问函数。通过测量执行时间来比较性能。我从一个包含32位整数(64 MiB)的大数组开始。
我跳过索引不为八的倍数的整数:这样可以最小化“缓存行”效应。代码如下:
type DataStruct struct { a, b, c, d, e, f, g, h uint32 } var arr []DataStruct for j := 0; j < arraySize; j++ { sum += arr[indices[j]].a // 仅访问第一个字段 }
在我的苹果笔记本上运行该程序,发现所有操作都比纯随机访问快得多。这说明我们的处理器在预测数据访问方面表现非常出色。