05-02-源码-Boehm GC标记阶段

源码深度:Boehm GC标记阶段

元数据

  • 系列:Unity和C#的GC剖析与对比
  • 章节:05-源码深度-Boehm GC与IL2CPP
  • 编号:05-02
  • 标签:Boehm GC, 保守标记, Root发现, Partial Clean, 标记栈
  • 阅读时间:~30分钟
  • 依赖知识:05-01数据结构

概述

Boehm GC的标记阶段是其最核心的组件。与SGen的精确标记不同,Boehm GC使用保守标记算法:不依赖对象的精确类型信息,而是将对象体中所有看起来像指针的word都当作潜在引用。这种设计使得Boehm GC可以与任意C/C++代码集成,但也带来了伪指针风险和扫描开销。本文深入分析保守标记算法的实现、根发现机制、Partial Clean优化以及标记栈管理。

保守标记算法

标记阶段入口

// gc_mark.c // 标记阶段入口 void GC_mark_roots (void) { // 1. 清除所有标记位 GC_clear_marks_all(); // 2. 初始化标记栈 GC_mark_stack_init(); // 3. 推送根集合 GC_push_roots(); // 4. 处理标记栈 while (!GC_mark_stack_empty()) { GC_mark_one_step(); } // 5. 处理弱引用和finalizer GC_mark_finalize(); }

清除所有标记位

// gc_mark.c static void GC_clear_marks_all (void) { // 遍历所有堆段,清除标记位图 for (int i = 0; i < GC_proc_instance.heap_sects_count; i++) { hblk *h = GC_proc_instance.heap_sects[i]; hblkhdr *hdr = GC_get_header((ptr_t)h); // 清除标记位图 memset(hdr->hb_marks, 0, sizeof(hdr->hb_marks)); } }

标记单个对象

// gc_mark.c // 保守标记:检查地址是否指向堆内对象,如果是则标记 static GC_bool GC_mark_one (ptr_t addr) { // 1. 检查是否在GC堆内 hblkhdr *hdr = GC_get_header(addr); if (!hdr) { return FALSE; // 不在堆内,不是对象指针 } // 2. 计算堆块基址 hblk *h = (hblk*)((ptr_t)addr & ~(HBLKSIZE - 1)); // 3. 检查地址是否对齐到对象边界 size_t obj_size = hdr->hb_obj_size; if (obj_size == 0) { // 大对象:整个堆块是一个对象 // 地址必须在对象起始位置 if ((ptr_t)addr != (ptr_t)h) { return FALSE; } } else { // 检查地址是否对齐到对象大小 size_t offset = (ptr_t)addr - (ptr_t)h; if (offset % obj_size != 0) { // 不对齐:可能是指向对象内部的指针 // 保守处理:找到对象起始位置 addr = (ptr_t)h + (offset / obj_size) * obj_size; } } // 4. 检查是否已标记 if (IS_MARKED(hdr, addr)) { return FALSE; // 已标记,跳过 } // 5. 设置标记位 SET_MARK(hdr, addr); // 6. 如果对象可能含指针,压入标记栈 if (hdr->hb_obj_kind != PTRFREE) { GC_mark_stack_push(addr, hdr); } return TRUE; }

保守扫描对象体

// gc_mark.c // 扫描对象体中的所有word,保守地标记可能的引用 static void GC_scan_object_body (ptr_t obj, hblkhdr *hdr) { size_t obj_size = hdr->hb_obj_size; if (obj_size == 0) { // 大对象:获取实际大小 obj_size = GC_get_large_obj_size(obj); } // 遍历对象体中每个word ptr_t end = obj + obj_size; for (ptr_t p = obj; p < end; p += sizeof(word)) { word value = *(word*)p; // 保守检查:是否指向堆内 if (GC_is_heap_ptr(value)) { GC_mark_one((ptr_t)value); } } }

标记栈处理

// gc_mark.c // 标记栈:存储待扫描的对象(灰色节点) typedef struct mark_stack_entry { ptr_t obj; // 对象地址 hblkhdr *hdr; // 堆块头 size_t scan_offset; // 已扫描到的偏移(增量标记用) } mark_stack_entry; static mark_stack_entry *GC_mark_stack; static mark_stack_entry *GC_mark_stack_top; static mark_stack_entry *GC_mark_stack_limit; // 压入标记栈 static void GC_mark_stack_push (ptr_t obj, hblkhdr *hdr) { if (GC_mark_stack_top >= GC_mark_stack_limit) { GC_mark_stack_grow(); } GC_mark_stack_top->obj = obj; GC_mark_stack_top->hdr = hdr; GC_mark_stack_top->scan_offset = 0; GC_mark_stack_top++; } // 弹出并处理一个标记栈条目 static void GC_mark_one_step (void) { GC_mark_stack_top--; mark_stack_entry *entry = GC_mark_stack_top; // 保守扫描对象体 GC_scan_object_body(entry->obj, entry->hdr); } // 标记栈扩容 static void GC_mark_stack_grow (void) { size_t old_size = GC_mark_stack_limit - GC_mark_stack; size_t new_size = old_size * 2; mark_stack_entry *new_stack = (mark_stack_entry*)GC_alloc_os_memory( new_size * sizeof(mark_stack_entry), GC_PROT_READ | GC_PROT_WRITE); memcpy(new_stack, GC_mark_stack, old_size * sizeof(mark_stack_entry)); // 更新指针 GC_mark_stack_top = new_stack + (GC_mark_stack_top - GC_mark_stack); GC_mark_stack = new_stack; GC_mark_stack_limit = new_stack + new_size; }

Root发现机制

根集合组成

// gc_root.c // Boehm GC的根集合由以下部分组成: // 1. 线程栈(包括寄存器) // 2. 静态数据区(.data, .bss段) // 3. 通过GC_add_roots注册的自定义根 // 4. GC Handle表 void GC_push_roots (void) { // 1. 扫描所有线程的栈和寄存器 GC_push_all_thread_stacks(); // 2. 扫描静态数据区 GC_push_roots_static(); // 3. 扫描注册的自定义根 GC_push_roots_custom(); // 4. 扫描GC Handle表 GC_push_roots_handles(); }

线程栈扫描

// gc_pthread.c // 扫描所有线程的栈 static void GC_push_all_thread_stacks (void) { // 遍历所有注册的线程 GC_thread *thread = GC_thread_list; while (thread) { // 获取线程的栈范围 ptr_t stack_bottom = thread->stack_base; ptr_t stack_top = thread->stack_ptr; // 当前栈指针 // 保存的寄存器上下文 ptr_t regs = (ptr_t)&thread->context; size_t regs_size = sizeof(thread->context); // 1. 保守扫描寄存器 GC_push_all(regs, regs + regs_size); // 2. 保守扫描栈 GC_push_all_stack(stack_bottom, stack_top); thread = thread->next; } }

保守栈扫描详解

// gc_root.c // 保守扫描一段内存区域 void GC_push_all (ptr_t bottom, ptr_t top) { ptr_t p; // 遍历区域中每个word for (p = bottom; p < top; p += sizeof(word)) { word value = *(word*)p; // 快速过滤:检查地址范围 // 优化:先检查是否在堆段的大致范围内 if (GC_quick_is_heap_ptr(value)) { // 精确检查:页表查找 if (GC_is_heap_ptr(value)) { // 标记对象 GC_mark_one((ptr_t)value); } } } } // 快速过滤:使用地址范围检查 static inline GC_bool GC_quick_is_heap_ptr (word addr) { // 检查是否在堆的大致地址范围内 // 这是一个快速但不精确的检查 return (addr >= GC_heap_addr_min && addr < GC_heap_addr_max); }

静态数据区扫描

// gc_root.c // 扫描静态数据区(.data和.bss段) static void GC_push_roots_static (void) { struct roots_static *root = GC_proc_instance.static_roots; while (root) { GC_push_all(root->start, root->end); root = root->next; } } // 注册静态数据区(在GC初始化时调用) void GC_register_static_roots (void) { // .data段 extern char __data_start[]; extern char __data_end[]; GC_add_roots(__data_start, __data_end); // .bss段 extern char __bss_start[]; extern char __bss_end[]; GC_add_roots(__bss_start, __bss_end); }

GC Handle扫描

// gc_handle.c // Boehm GC支持类似.NET的GC Handle机制 // 在IL2CPP中用于实现GCHandle、WeakReference等 typedef struct { void *obj; // 指向的对象 int type; // Handle类型 // 类型: NORMAL, PINNED, WEAK, WEAK_TRACK_RESURRECTION } GC_handle_entry; static GC_handle_entry *GC_handle_table; static size_t GC_handle_table_size; static void GC_push_roots_handles (void) { for (size_t i = 0; i < GC_handle_table_size; i++) { if (GC_handle_table[i].obj) { switch (GC_handle_table[i].type) { case GC_HANDLE_NORMAL: case GC_HANDLE_PINNED: // 强引用:标记对象 GC_mark_one((ptr_t)GC_handle_table[i].obj); break; case GC_HANDLE_WEAK: case GC_HANDLE_WEAK_TRACK: // 弱引用:不标记,在标记完成后处理 break; } } } }

Partial Clean机制

什么是Partial Clean

Partial Clean是Boehm GC的一种优化策略:在标记阶段完成后、清扫阶段之前,对某些"明显不可达"的堆块进行快速清理,减少完整清扫的工作量。

// gc_reclaim.c // Partial Clean:在标记完成后立即回收某些块 void GC_partial_clean (void) { // 遍历所有堆块 for (int i = 0; i < GC_proc_instance.heap_sects_count; i++) { hblk *h = GC_proc_instance.heap_sects[i]; hblkhdr *hdr = GC_get_header((ptr_t)h); // 只处理PTRFREE类型的块 // PTRFREE对象不含指针,可以快速判断可达性 if (hdr->hb_obj_kind == PTRFREE) { GC_partial_clean_block(h, hdr); } } } static void GC_partial_clean_block (hblk *h, hblkhdr *hdr) { size_t obj_size = hdr->hb_obj_size; int obj_count = HBLKSIZE / obj_size; // 检查块中每个对象的标记状态 int all_unmarked = TRUE; int all_marked = TRUE; for (int i = 0; i < obj_count; i++) { ptr_t obj = (ptr_t)h + i * obj_size; if (IS_MARKED(hdr, obj)) { all_unmarked = FALSE; } else { all_marked = FALSE; // 回收未标记的对象 GC_reclaim_obj(obj, obj_size); } } if (all_unmarked) { // 整个块都不可达,可以归还给OS或加入空闲块池 GC_free_hblk(h); } else if (all_marked) { // 整个块都存活,跳过清扫 hdr->hb_last_gc = GC_proc_instance.gc_no; } }

Partial Clean的优势

// gc_reclaim.c // Partial Clean减少了完整清扫的工作量 // 特别对PTRFREE对象(如byte[], int[]等)效果显著 // 性能对比: // 不使用Partial Clean: // 标记 -> 清扫所有块 -> 重建空闲链表 // 时间复杂度: O(堆大小) // // 使用Partial Clean: // 标记 -> Partial Clean(PTRFREE块) -> 清扫剩余块 // PTRFREE块可以快速处理(无需扫描对象体) // 时间复杂度: O(非PTRFREE块大小) + O(PTRFREE块数)

标记阶段优化

标记位图缓存优化

// gc_mark.c // Boehm GC使用位图标记,与SGen的vtable低位标记不同 // 位图的优势:不修改对象体,对缓存友好 // 劣势:需要额外的内存开销 // 标记位图布局: // 堆块(4KB): // +---+---+---+---+---+---+---+---+---+---+ // | O0| O1| O2| O3| O4| O5| O6| O7| ... | // +---+---+---+---+---+---+---+---+---+---+ // // 标记位图(每个对象1位): // +---+---+---+---+---+---+---+---+ // | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | (O0存活, O2存活, O6存活) // +---+---+---+---+---+---+---+---+

批量标记

// gc_mark.c // 批量处理标记栈,减少函数调用开销 static void GC_mark_batch (int batch_size) { mark_stack_entry batch[GC_MARK_BATCH_SIZE]; int count = 0; // 批量弹出 while (count < batch_size && !GC_mark_stack_empty()) { GC_mark_stack_top--; batch[count++] = *GC_mark_stack_top; } // 批量处理 for (int i = 0; i < count; i++) { GC_scan_object_body(batch[i].obj, batch[i].hdr); } }

并行标记

// gc_par_mark.c // Boehm GC支持并行标记(可选) typedef struct { mark_stack_entry *local_stack; mark_stack_entry *local_stack_limit; mark_stack_entry *local_stack_top; // 全局共享标记栈(用于工作窃取) mark_stack_entry *global_stack; mono_mutex_t *global_lock; int worker_id; volatile int *done_flag; } GC_mark_worker; static void GC_mark_parallel (void) { int n_workers = GC_proc_instance.mark_parallelism; GC_mark_worker *workers = (GC_mark_worker*)calloc(n_workers, sizeof(GC_mark_worker)); // 初始化工作线程 for (int i = 0; i < n_workers; i++) { workers[i].worker_id = i; workers[i].global_stack = GC_mark_stack; workers[i].global_lock = &GC_mark_stack_lock; workers[i].done_flag = &GC_mark_done; // 分配本地标记栈 workers[i].local_stack = (mark_stack_entry*)GC_alloc_os_memory( GC_MARK_STACK_INIT_SIZE * sizeof(mark_stack_entry), GC_PROT_READ | GC_PROT_WRITE); workers[i].local_stack_limit = workers[i].local_stack + GC_MARK_STACK_INIT_SIZE; workers[i].local_stack_top = workers[i].local_stack; } // 启动工作线程 for (int i = 1; i < n_workers; i++) { GC_start_mark_thread(&workers[i]); } // 主线程也参与标记 GC_mark_worker_main(&workers[0]); // 等待所有工作线程完成 GC_wait_mark_workers_done(); }

弱引用与Finalizer处理

弱引用处理

// gc_mark.c // 标记完成后处理弱引用 void GC_mark_finalize (void) { // 1. 处理弱引用 GC_process_weak_refs(); // 2. 处理finalizer队列 GC_process_finalize_queue(); } static void GC_process_weak_refs (void) { // 遍历所有弱引用 for (size_t i = 0; i < GC_handle_table_size; i++) { if (GC_handle_table[i].type == GC_HANDLE_WEAK || GC_handle_table[i].type == GC_HANDLE_WEAK_TRACK) { void *obj = GC_handle_table[i].obj; if (obj && !GC_is_marked(obj)) { // 目标不可达,清空弱引用 GC_handle_table[i].obj = NULL; if (GC_handle_table[i].type == GC_HANDLE_WEAK_TRACK) { // 加入复活队列 GC_add_to_resurrection_queue(obj); } } } } }

Finalizer处理

// gc_mark.c static void GC_process_finalize_queue (void) { // 遍历finalizer队列 GC_finalize_entry *entry = GC_finalize_list; GC_finalize_entry *prev = NULL; while (entry) { GC_finalize_entry *next = entry->next; if (!GC_is_marked(entry->obj)) { // 对象不可达,需要finalizer // 临时标记对象(使其在本次GC中存活) GC_mark_one((ptr_t)entry->obj); // 移到待执行队列 GC_add_to_to_finalize(entry->obj, entry->fn); // 从finalizer队列中移除 if (prev) prev->next = next; else GC_finalize_list = next; } else { prev = entry; } entry = next; } }

保守标记的误标记分析

伪指针场景

// 伪指针示例: // 栈上有一个整数值 0x7F3A0010 // 恰好堆中有一个对象地址为 0x7F3A0010 // 保守GC会将这个整数误认为对象引用,标记该对象为存活 // 伪代码展示: void foo() { int x = 0x7F3A0010; // 一个整数值 // ... 此时GC触发 // GC扫描栈,发现x的值看起来像堆地址 // GC标记地址0x7F3A0010处的对象为存活 // 即使该对象实际上不可达 }

误标记的影响

// 误标记导致的问题: // 1. 内存泄漏:不可达对象被误标记为存活,无法回收 // 2. 级联效应:被误标记对象的引用也被标记 // 导致更多对象无法回收 // 3. 不可预测性:误标记取决于运行时的栈/寄存器状态 // 同一程序在不同运行中可能有不同的GC行为 // 缓解措施: // 1. 黑名单机制:记录可疑地址,避免在这些地址分配对象 // 2. 对齐检查:只将对齐到对象边界的地址视为有效引用 // 3. 堆块隔离:不同类型的对象分配在不同区域 // 减少伪指针命中的概率

总结

Boehm GC标记阶段流程: GC_mark_roots() | +-- GC_clear_marks_all() // 清除所有标记位 | +-- GC_push_roots() // 推送根集合 | |-- GC_push_all_thread_stacks() // 扫描线程栈+寄存器 | |-- GC_push_roots_static() // 扫描静态数据区 | |-- GC_push_roots_custom() // 扫描自定义根 | +-- GC_push_roots_handles() // 扫描GC Handle | +-- while (!mark_stack_empty) // 处理标记栈 | +-- GC_scan_object_body() // 保守扫描对象体 | +-- 遍历每个word | +-- GC_is_heap_ptr() // 检查是否指向堆 | +-- GC_mark_one() // 标记对象+压栈 | +-- GC_mark_finalize() // 处理弱引用和finalizer |-- GC_process_weak_refs() // 清空指向不可达对象的弱引用 +-- GC_process_finalize_queue() // 临时标记需finalizer的对象
特性保守标记精确标记(SGen)
类型信息不需要需要描述符
扫描开销高(扫描所有word)低(只扫描引用字段)
误标记风险有(伪指针)无
对象移动不支持支持
适用场景C/C++混合纯托管代码

Boehm GC的保守标记策略是其最大的设计决策,既带来了通用性优势,也引入了性能和正确性方面的挑战。下一章将分析分配与碎片管理。

相关新闻

STM32F1系列深度实战:从裸机开发到工业级可靠性设计

STM32F1系列深度实战:从裸机开发到工业级可靠性设计

1. 项目概述&#xff1a;为什么STM32F1系列至今仍是嵌入式开发者的“第一块砖”如果你刚打开Keil MDK&#xff0c;新建工程时下拉列表里第一个跳出来的芯片型号是STM32F103C8T6&#xff0c;别怀疑——这不是巧合&#xff0c;而是整整十五年工业验证、教学沉淀与产线惯性共同写就…

2026/10/20 10:57:55 阅读更多 →
实时数据中台实战:从Kafka到Flink的架构设计与故障排查

实时数据中台实战:从Kafka到Flink的架构设计与故障排查

1. 为什么大家都在谈实时数据中台——先把这件事想清楚这几年只要聊到数据基建&#xff0c;“实时数据中台”几乎是绕不开的词。我做数据开发也有不少年头了&#xff0c;前前后后参与过好几套数据平台的从零搭建&#xff0c;从最早的Hive离线数仓到后来的Flink实时链路&#xf…

2026/10/20 10:57:55 阅读更多 →
LangGraph生产级智能体实战:核心组件与编排经验

LangGraph生产级智能体实战:核心组件与编排经验

做智能体项目有一段时间了&#xff0c;最近两个月基本上把LangGraph的重要组件挨个用了一遍。最开始看官方文档的时候&#xff0c;脑子里全是新名词&#xff1a;State、Node、Edge、Conditional Edge、Checkpointer、Tool、Memory&#xff0c;说实话有点劝退。但实际跑通一个完…

2026/10/20 10:56:53 阅读更多 →

最新新闻

基于深度学习的新闻摘要生成:从Encoder-Decoder到注意力机制的实践

基于深度学习的新闻摘要生成:从Encoder-Decoder到注意力机制的实践

又到了一年一度的毕设季&#xff0c;后台好几个读者问我同一个题目&#xff1a;“基于深度学习的新闻摘要生成算法实现”。这个题选得挺聪明&#xff0c;新闻摘要属于文本生成里最有代表性的任务之一&#xff0c;数据好找、效果直观、还能往上堆注意力机制和预训练模型&#xf…

2026/10/20 11:59:57 阅读更多 →
pvbrowser构建指南:Qt 5.15.2定制编译与WebEngine禁用实践

pvbrowser构建指南:Qt 5.15.2定制编译与WebEngine禁用实践

简介&#xff1a;本资源为开源SCADA组态软件pvbrowser的完整安装与开发包&#xff0c;面向工业自动化初学者、QT开发者及中小型项目实施工程师&#xff0c;解决跨平台监控系统快速搭建、设备协议对接与图形化组态开发等核心需求。压缩包为ZIP格式&#xff0c;大小27.65MB&#…

2026/10/20 11:59:57 阅读更多 →
NeRF工业化落地:从玩具级Demo到生产级应用的工程化实践

NeRF工业化落地:从玩具级Demo到生产级应用的工程化实践

1. 先从“玩具级”说起&#xff1a;NeRF到底卡在了哪里任何一个新技术&#xff0c;从论文走到生产线&#xff0c;中间隔着一条叫“工程化”的河。神经辐射场这两年被提起的频率极高&#xff0c;但很多团队真正试过之后的第一反应往往是“效果确实惊艳&#xff0c;可用不起来”。…

2026/10/20 11:59:57 阅读更多 →
MongoDB到HBase迁移实战:Kettle选型与避坑指南

MongoDB到HBase迁移实战:Kettle选型与避坑指南

一个多月前&#xff0c;我们内部的数据平台需要把线上MongoDB里的用户行为明细同步到HBase。下游要做多维统计和特征加工&#xff0c;MongoDB的灵活文档模型开发期确实舒服&#xff0c;但大范围scan和列式存储场景并不合适&#xff1b;HBase能扛住高吞吐的随机读写与批量扫描&a…

2026/10/20 11:59:57 阅读更多 →
Dify完全指南:从零搭建LLM应用开发平台(涵盖RAG、Agent与部署)

Dify完全指南:从零搭建LLM应用开发平台(涵盖RAG、Agent与部署)

这段时间我一直在折腾各种大模型相关的应用&#xff0c;前后用原生代码搭过客服机器人&#xff0c;也试过纯前端接模型接口做演示 Demo&#xff0c;最后发现一个很尴尬的事实&#xff1a;真正耗时间的不是“调用模型”&#xff0c;而是那些跟业务没半毛钱关系的工程问题。比如多…

2026/10/20 11:59:57 阅读更多 →
Pygame性能优化:把静态障碍画进背景并用Mask碰撞检测

Pygame性能优化:把静态障碍画进背景并用Mask碰撞检测

我先说自己踩过的一个坑&#xff1a;做一个小游戏&#xff0c;十几种障碍物各自做成独立精灵对象&#xff0c;视觉上没问题&#xff0c;但数量一多&#xff0c;每帧要遍历、更新、碰撞检测&#xff0c;开销一下上去了。后来改成把所有障碍直接画到背景面上&#xff0c;再配合 m…

2026/10/20 11:58:56 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →