cad.net 利用句柄遍历+并行遍历数据库
说明
这是一种利用句柄遍历数据库的方式,句柄是经过cad内部分配器进行递增.
在某些情况下(可能是天正环境导致),用此方法遍历数据库奇慢,会在循环中一直自增,为了停止它,我使用了一个变量.
第二种奇慢是acad08上面,acad程序员把断言和vs输出弄反了,导致debug模式一直弹出miss信息.
为了遍历可能的千万级句柄,使用Parallel.For并行遍历转换句柄到id,能够更快实现对id获取.
信息挖掘
ObjectIds上缓慢的迭代
同时因为id = db.GetObjectId(false, handle, 0);
在acad08的表现非常慢,所以我被迫调用了Arx函数.
当我和e大做完了之后,我发现上面论坛链接的第二页居然有....
edata :打开文件后,句柄是2c5,继续画直线:2c6...2c7...2c8
如果此时保存了,就从313开始,重新打开后,句柄浪费了75个左右...
ctrl+c乱增,co就不会乱增...
遍历组码
同时发现这是遍历dwg所有组码的方式,因为entget并不能获取动态块组码信息,所以才需要它.
貌似还可以ent.DxfOut
有了组码,就可以获取动态块可见性名称了,其他的或许也可以...
分析动态块组码见动态块专篇
Arx代码见e大博文遍历动态块块定义的可见性名称方法
代码
#if !HC2020
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
#else
using GrxCAD.DatabaseServices;
using GrxCAD.Runtime;
using GrxCAD.EditorInput;
using Acap = GrxCAD.ApplicationServices.Application;
#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace JoinBox.试验库
{
public class CmdTest_FastIterationClass
{
[CommandMethod("CmdTest_FastIteration")]
public void CmdTest_FastIteration()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
ed.WriteMessage($"{Environment.NewLine}利用句柄找到动态块可见性的名称");
var dic = db.HandleToObjectId();
int entNum = 0;
using var tr = db.TransactionManager.StartTransaction();
foreach (var dicItem in dic) {
var id = dicItem.Value;
if (!id.IsOk()) continue;
string dxfName = id.GetDxfName();
ed.WriteMessage($"{Environment.NewLine}图元{++entNum}:DxfName={dxfName};句柄={dicItem.Key};Id={id}");
if (dxfName == "BLOCKVISIBILITYPARAMETER")
{
AcdbAdsHelper.AcdbGetAdsName(out AdsName eName, id);
var elist = AcdbAdsHelper.AcdbEntGet(ref eName);
if (elist == IntPtr.Zero) {
using var rb = new ResultBuffer();
elist = AcdbAdsHelper.AcdbEntGetX(ref eName, rb.UnmanagedObject);
}
if (elist == IntPtr.Zero)
continue;
using var rb2 = ResultBuffer.Create(elist, true);
foreach (var tv in rb2) {
if (tv.TypeCode == 303)//动态块的可见性参数被改的名称记录位置
PrintDxf(tv, ed);
}
}
}
ed.WriteMessage($"{Environment.NewLine}结束了!");
}
public void PrintDxf(TypedValue ty, Editor ed)
{
ed.WriteMessage($"{Environment.NewLine}TypeCode:{ty.TypeCode};;;Value:{ty.Value}");
}
}
public static class DatabaseHelper
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetDxfName(this ObjectId id)
{
string dxfName;
#if !NET35
dxfName = id.ObjectClass.DxfName;
#else
// DBObject obj = tr.GetObject(id, OpenMode.ForRead);节约事务参数
// dxfName = RXClass.GetClass(obj.GetType()).DxfName;//这里并不能准确获取到dxfName
using DBObject obj = id.Open(OpenMode.ForRead);
dxfName = obj.GetRXClass().DxfName;
#endif
return dxfName;
}
/// <summary>
/// 遍历句柄获取数据库的ids
/// 可能是已经删除的,不一定满足id.IsOk()
/// </summary>
/// <param name="db"></param>
/// <param name="ed">控制是否打印时间</param>
public static Dictionary<Handle, ObjectId> HandleToObjectId(this Database db, Editor ed = null)
{
Dictionary<Handle, ObjectId> dictResult = new();
DateTime start = DateTime.Now;//时间计数
long time = 0; //时间计数
db.TraverseHandle(h => {
Handle handle = new(h);
var id = db.TryGetObjectId(handle);
if (id != ObjectId.Null)
dictResult.Add(handle, id);
if (ed != null) {
//计时输出
var len = DateTime.Now - start;
if (len.Seconds != time && len.Seconds % 5 == 0)//5秒输出一次
{
time = len.Seconds;
ed.WriteMessage($"{Environment.NewLine}数量: {h},已经过了{len.Days}天,{len.Hours}小时,{len.Minutes}分,{len.Seconds}秒");
ed.UpdateScreenEx();//此函数写在 https://www.cnblogs.com/JJBox/p/11354224.html
}
}
});
return dictResult;
}
/// <summary>
/// 遍历句柄
/// </summary>
/// <param name="db">数据库</param>
/// <param name="action">扔出去个迭代句柄数字</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void TraverseHandle(this Database db, Action<long> action)
{
#if !NET35
// 遍历块表头尾(不是数据库头尾)
// 青蛙说下面这个注释的不能用了,不知道这个偏移量计算方式
// 是不是new-old==0就是新cad开dwg07的偏移量计算呢?又或者是第一个转换id无效,就跳转到第二个?
// var entLast = Autodesk.AutoCAD.Internal.Utils.EntLast().OldIdPtr;
// var entStart = db.BlockTableId.OldIdPtr;
var entLast = Autodesk.AutoCAD.Internal.Utils.EntLast().Handle.Value;
var entStart = db.BlockTableId.Handle.Value;
if (IntPtr.Size == 4) {
var e = entLast.ToInt32();//防止句柄递增
for (var i = entStart.ToInt32(); i < e; i++)//可以改为并行Parallel.For
action(i);
}
else if (IntPtr.Size == 8) {
var e = entLast.ToInt64();//防止句柄递增
for (var i = entStart.ToInt64(); i < e; i++)//可以改为并行Parallel.For
action(i);
}
#else
// 遍历块表头到数据库尾
// (按理说应该0到数据库尾/或者块表头尾,这个版本应该是缺失了块表尾API)
var dbLast = db.Handseed.Value;//防止句柄递增
var entStart = db.BlockTableId.Handle.Value;
for (var i = entStart; i < dbLast; i++)//可以改为并行Parallel.For
action(i);
#endif
}
}
public static class TryGetObjectIdHelper
{
/*
* id = db.GetObjectId(false, handle, 0);
* 参数意义: db.GetObjectId(如果没有找到就创建,句柄号,标记..将来备用)
* 在vs的输出会一直抛出:
* 引发的异常:“Autodesk.AutoCAD.Runtime.Exception”(位于 AcdbMgd.dll 中)
* "eUnknownHandle"
* 这就是为什么慢的原因,所以直接运行就好了!而Debug还是需要用arx的API替代.
*/
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb17.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QAE?AW4ErrorStatus@Acad@@AAVAcDbObjectId@@_NABVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId17x32(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb17.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QEAA?AW4ErrorStatus@Acad@@AEAVAcDbObjectId@@_NAEBVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId17x64(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb18.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QAE?AW4ErrorStatus@Acad@@AAVAcDbObjectId@@_NABVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId18x32(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
[System.Security.SuppressUnmanagedCodeSecurity]
[DllImport("acdb18.dll", CallingConvention = CallingConvention.ThisCall/*08的调用约定 高版本是__cdecl*/,
EntryPoint = "?getAcDbObjectId@AcDbDatabase@@QEAA?AW4ErrorStatus@Acad@@AEAVAcDbObjectId@@_NAEBVAcDbHandle@@K@Z")]
extern static int getAcDbObjectId18x64(IntPtr db, out ObjectId id, [MarshalAs(UnmanagedType.U1)] bool createnew, ref Handle h, uint reserved);
/// <summary>
/// 句柄转id,NET35(08~12)专用的
/// </summary>
/// <param name="db">数据库</param>
/// <param name="handle">句柄</param>
/// <param name="id">返回的id</param>
/// <param name="createIfNotFound">不存在则创建</param>
/// <param name="reserved">保留,用于未来</param>
/// <returns>成功0,其他值都是错误.可以强转ErrorStatus</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int GetAcDbObjectId(IntPtr db, Handle handle, out ObjectId id, bool createIfNotFound = false, uint reserved = 0)
{
id = ObjectId.Null;
switch (Acap.Version.Major) {
case 17: {
if (IntPtr.Size == 4)
return getAcDbObjectId17x32(db, out id, createIfNotFound, ref handle, reserved);
else
return getAcDbObjectId17x64(db, out id, createIfNotFound, ref handle, reserved);
}
case 18: {
if (IntPtr.Size == 4)
return getAcDbObjectId18x32(db, out id, createIfNotFound, ref handle, reserved);
else
return getAcDbObjectId18x64(db, out id, createIfNotFound, ref handle, reserved);
}
}
return -1;
}
/// <summary>
/// 句柄转id
/// </summary>
/// <param name="db">数据库</param>
/// <param name="handle">句柄</param>
/// <returns>id</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ObjectId TryGetObjectId(this Database db, Handle handle)
{
#if !NET35
//高版本直接利用
var es = db.TryGetObjectId(handle, out ObjectId id);
//if (!es)
#else
var es = GetAcDbObjectId(db.UnmanagedObject, handle, out ObjectId id);
//if (ErrorStatus.OK != (ErrorStatus)es)
#endif
return id;
}
/// <summary>
/// 此命令用来测试:内部调用NET35(08~12)专用的 GetAcDbObjectId,
/// 外部调用封装的 TryGetObjectId.
/// </summary>
[CommandMethod("CmdTest_handloop")]
public static void CmdTest_handloop()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
// 遍历数据库头尾,串行例子
for (long i = 0; i < db.Handseed.Value; i++) {
Handle handle = new(i);
var es = GetAcDbObjectId(db.UnmanagedObject, handle, out ObjectId id);
if (ErrorStatus.OK == (ErrorStatus)es)
ed.WriteMessage("\n执行成功");
if (id.IsNull) continue;
if (id.IsValid) ed.WriteMessage($"\n有效ID:{id},句柄是{handle}");
else ed.WriteMessage($"\n无效ID:{id},句柄是{handle}");
if (id.IsErased) {
ed.WriteMessage("是已经删除的ID");
continue;
}
using var tr = db.TransactionManager.StartTransaction();
using DBObject obj = tr.GetObject(id, OpenMode.ForRead);
if (obj == null) continue;
ed.WriteMessage($"\n句柄={handle};DxfName={id.GetDxfName(tr)}");
//九个符号表
if (obj is BlockTable)
ed.WriteMessage("\nID是table");
}
}
}
}
并行例子
遍历数据库头尾,并行例子
并行就不要开事务,用Open只读,归类到线程安全字典,从而制造数据库索引.
不同的句柄get的id可能是重复的.
OpenMode.ForRead限制256个只读器,似乎超过也没有问题,不明白为什么写这个出来...
net8的有序并发容器其他net版本可以拷贝这个代码到自己的工程上面.或者采用java的跳表/ConcurrentHashSet之类的,否则下面无法消重
// 分类索引的别名<dxfName,图元集合>集合选择跳表或者其他线程安全排序容器(排序为了消重,不同句柄可能指向相同id)
using EntityMapType = ConcurrentDictionary<string,ConcurrentHashSet<ObjectId>>();
public class Test {
static EntityMapType EntityMap = [];
static ConcurrentQueue<(Database, long)> queue = new();
static long atomicCount = 0;
static LoopState state = new(); //并行中断标记
[CommandMethod("CmdTest_handloop")]
public static void CmdTest_handloop()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
Stopwatch sw = new Stopwatch();
sw.Restart();
// 消费者线程,由CPU核心数确定数量
for (int i = 0; i < 16; i++)
new Thread(CounterThread).Start();
// 生产者线程,不限制可以直接DoWork()
Parallel.For(0, db.Handseed.Value, handleIndex => {
queue.Enqueue((db, handleIndex));
});
/* 并行会阻塞,我们组建索引只需要打开图纸的时候让异步组建,使用再问state完成没,不需要并行阻塞.但是要解决跨线程获取数据库问题.
for(int i = 0;i < 16; i++)
new Thread(CounterThread2).Start();
*/
ed.WriteMessage($"耗时={sw.ElaspseString()}");
}
// 消费者线程,从队列中取出数据
static void CounterThread()
{
while (state.IsRun) {
long atomicValue = Interlocked.Read(ref atomicCount); // 多个线程会取到相同值
if (atomicValue < 256L // 限制只读器
&& queue.TryDequeue(out (Database, long) result)) {
Interlocked.Increment(ref atomicCount);
DoWork(result.Item1, result.Item2);
Interlocked.Decrement(ref atomicCount);
continue;
}
Thread.Sleep(1);
}
}
static void DoWork(Database db, long handleIndex)
{
Handle handle = new(handleIndex);
var es = (ErrorStatus)TryGetObjectIdHelper.getAcDbObjectId19x64(db.UnmanagedObject, out ObjectId id, false, ref handle, 0);
if (ErrorStatus.OK != es || id.IsNull || id.IsErased || id.IsEffectivelyErased)
return;
// 如果要打开id,那么要采取open/close方法,或者using.
using DBObject obj = id.Open(OpenMode.ForRead);
if(obj is Entity ent){...}
var dxfName = id.ObjectClass.Name;
EntityMap.AddOrUpdate(dxfName,new ConcurrentBag<ObjectId>(){id},(key,bag)=> { bag.Add((id));return bag;});
}
// 写数据库开启完成的事件上面,开启n个线程,全部任务完成会设置state,不使用并行阻塞.
static void CounterThread2()
{
/* 多线程内部get database会报错,为了避免此问题,将用数据库启动事件代替
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
var seed = db.Handseed.Value;
这里仅仅是测试,为了性能应该直接:
var value = Interlocked.Increment(ref atomicCount)
而不是像下面一样使用CAS,大量比较失败会变卡
*/
while (state.IsRun) {
var value = atomicCount; // 多个线程可能重复记录value,如果替换不成功跳转,否则用原值进行下一步.
if (Interlocked.CompareExchange(ref value, value + 1, atomicCount) != atomicCount)
continue;
if (value < seed) DoWork(db, value);
else state.Stop();
}
}
}//class
数据库启动事件...伪代码
// 分类索引的别名<dxfName,图元集合>集合选择跳表或者其他线程安全排序容器(排序为了消重,不同句柄可能指向相同id)
using EntityMapType = ConcurrentDictionary<string,ConcurrentHashSet<ObjectId>>();
class DatabaseOpenTest2 {
// 这里全局变量不要用Databse因为导致无法释放,所以用databaseFileName,
// 线程上下文问题在事件内穿透传值.
static Dictionary<string,(LoopState state,Task task)> IndexTask=[];
static Dictionary<string,EntityMapType> IndexMap=[];
public void Test() {
// 事件这里是单线程
// https://help.autodesk.com/view/OARX/2022/ENU/?guid=GUID-925C090F-C90F-4F1E-91F7-E39FF48E5655
editor.EndDwgOpen+=(s,e)=>{
var db = e.Databse;
var seed = db.Handseed.Value;
// 下面这两个对象生命周期会被字典转移到全局
var state = new LoopState();
EntityMapType entityMap=[];
// Task包装实现非阻塞并行,内部是线程不安全部分
var task = Task.Run(() => {
Parallel.For(0, seed/*穿透线程*/, i => {
// CPU密集型任务,构造索引...
DoWork(db,i);
entityMap.Add(dxfName,图元....);
});
state.Stop();
});
IndexTask.Add(db.FileName,(state,task));
IndexMap.Add(db.FileName,entityMap);
task.Start();
});
}
// 通过数据库名,获取分类索引.
public bool TryGetValue<T,V>(string dbFile, T key, out V value){
// 利用线程标记阻塞获取(什么情况可以非阻塞获取?好像没有,不如删掉状态,直接调用wait算了)
var d = IndexTask[dbFile];
if(d.state.IsStop()) d.task.Wait();
return IndexMap[dbFile].TryGetValue(key,out value);
}
}
缺省函数
// https://gitee.com/inspirefunction/ifoxcad/blob/v0.7/src/Basal/IFox.Basal.Shared/General/LoopState.cs
/// <summary>
/// 控制循环结束
/// </summary>
public class LoopState {
private const int PlsNone = 0;
private const int PlsExceptional = 1;
private const int PlsBroken = 2;
private const int PlsStopped = 4;
private const int PlsCanceled = 8;
private volatile int _flag = PlsNone;
public bool IsRun => _flag == PlsNone;
public bool IsExceptional => (_flag & PlsExceptional) == PlsExceptional;
public bool IsBreak => (_flag & PlsBroken) == PlsBroken;
public bool IsStop => (_flag & PlsStopped) == PlsStopped;
public bool IsCancel => (_flag & PlsCanceled) == PlsCanceled;
public void Exceptional() {
if ((_flag & PlsExceptional) != PlsExceptional)
_flag |= PlsExceptional;
}
public void Break() => _flag = PlsBroken;
public void Stop() => _flag = PlsStopped;
public void Cancel() => _flag = PlsCanceled;
public void Reset() => _flag = PlsNone;
}
四叉树
还记得四叉树原理吗?插入图元时候,当四个孩子节点都无法独立拥有它时(节点矩形全包含),这个图元就属于它们父亲,表现则是十字压着.
这个是一种朴素四叉树的概念,因为它太朴素了,以至于还有很多细节.
https://www.cnblogs.com/JJBox/p/15512317.html
细节一
插入:一个节点图元数量满足阈值n,才需要分裂到四个孩子,减少节点创建,从而减少递归爆栈.
也可以加多一个树深度阈值m,控制(acad八叉树系统变量是TreeMax,我们甚至可以复用它).这个时候聪明人肯定想,有没有自适应的呢?嘿嘿就是R树,而不是四叉树.
因为有阈值n存在,所以Point这种无面积类型也能加入四叉树.但是仔细想想,会发生一个阈值穿透问题,上层满了,分配到下层,下层又满了,又分配到下下层...
有序点集或者K树更小更快,Point3d三个值和Rect四个值内存占用代价有差异,所以单独存放一个结构更好.这个结构甚至可以利用一个叫做跳表的功能进行索引,这就是另一个话题了,我们跳过它.
而在四叉树节点内排序节点图元,查找的时候可以二分法或者SIMD遍历?那包围盒根据什么排序呢?
答案是不要这样做.阈值n存在,那么表示某些子节点能完全包含图元,因此有象限序,在超阈值之后能够非遍历父节点,存在break...似乎可以,不然通过阈值n控制分裂本身就是一种分类了,分类就是排序的一种,因此不需要这个设计,并且排序也会增加插入时间.
搜索:不需要判断是否超过阈值,没有的话超出自然没有子节点.
细节二
我在IFox上面实现过一个四叉树,发现了十字位置其实会退化成遍历,也就是当图元聚集在十字上面,超过了分裂阈值,四个孩子也无法分到图元.
此时怎么办呢?这就需要把节点内容给下面这个散列结构了,也就是我们要结构a套结构b了,皮裤套棉裤必是有缘故,不是棉裤太薄就是皮裤没毛.
空间哈希网格
一种叫空间哈希网格(SpaceHashGrid)结构能解决这个问题,它相当于把空间均匀分割成m*m网格,拉直成一条线.
通过图元AABB包围盒的"中点"转为HashCode命中数组索引,从而插入hashmap上,以此散列了十字位置的图元们.当然,你会发现"中点"被散列其实难以被矩形选择(腰部选择),反正你知道能被散列就行,下面计算会更详细.
然后此时你会发现,网格中除了十字位置被图元散列,大量的网格是空的,因此还得引入稀疏数组来实现这个鬼东西...(真是一环套一环,为了不更深入解释,我就跳过这个)
搜索:同样计算索引,然后就可以遍历节点内图元集合了.和细节一不同,需要判断超出阈值n,四叉树节点上面必然是空的,直接转入网格结构搜索.
缺点就是网格结构无法像四叉树八叉树一样反向扩容,那么做到四叉树节点内也不需要它反向扩容,逻辑完美...
插入
hash的计算:
假设我们把网格结构的横轴坐标1-100切割成十份,那么插入包围盒中点x=23.4,怎么知道它在哪一个网格呢?
100.0/10=10.0(单位间距)
23.4/10=2.34(向上取整)=3(网格索引)
这样就得到了X轴的,Y轴同理.
查询
在map中获取就只需要key是("X"+","+"Y")就能获取value了,可谓是简单极了.不过字符串拼接再转为hashcode,这个对于GC不友好,那么这个map为何使用hashmap呢?同时我们还存在一个问题,一个大图元跨几个空间,那么仅包围盒中点的节点记录图元id,是会存在缺失选择的,所以我们需要根据选择调整插入.(为什么缺失选择,想象一个圆形被栅格化,然后只有中点记录了图元id,边缘怎么也选中不了)
有两种做法插入图元id:
方法一:AABB包围盒四个Point节点加入图元id.
方法二:AABB包围盒围着的节点都加入图元id.
方案三:AABB包围盒都在网格内才属于网格,否则网格中链接一个大网格(另见大数据处理)
作为内存节约,肯定会觉得方案一更好,因此,我们就不能用hashmap["x,y"]方式获取了.
我们需要构造X值数组和Y值数组,搜索时候排除选框矩形左边不是范围,和右边不是范围,在中间就是选择范围,留下一个Xmin和Xmax,Y轴同理.
于是乎这个代码会变成SIMD+并行,然后在不支持SIMD的时候退化成二分法搜索xArray和yArray,这样就无敌了.
完整的节点分裂流程
加入图元到四叉树节点时,先判断是否超出阈值n,否就直接加入,是就判断四个儿子有没有一个能加入图元,能就进行分裂和分配.
剩下没能分配出去的,就是父节点拥有的,检查是否还超出阈值,如果超出了,就构建哈希网格.
完整的搜索
同细节二
参考
在地图上面就不一样了,他甚至用了希尔伯特曲线实现节点分裂.
https://halfrost.com/go_spatial_search
[SimonDev] https://b23.tv/SfkCJKN
就此ssget完全可以代替掉了.之前还觉得怎么屏蔽关键字,觉得键盘钩子先抓输入,然后转为对应事件就好了.后来发现了,自己构造索引基本上无敌.
并发空间哈希网格
因为dwg句柄范围可以并行处理,那么归类图元数据就成为瓶颈.
因此我们需要基于MapReduce思想优化,也就是先分区,并行分区任务,最后聚合.
类型字典结构缓存:
利用dxf特别慢: map<dxfName,Bag>十万8.8ms
因此改为: map<typeName,Bag>十万1.6ms
并发的Bag内部是每个线程一个List,然后串联起来的,还满符合复用线程的思想.
空间索引结构缓存:
由于四叉树存在SMO(Structure Modification Operation),所以它会频繁触发根锁和节点锁,因此需要找到一种少锁的结构.
先划分一个空间哈希网格,要求是n^4(同四叉树),此结构没有SMO.计算hash索引只是数值计算,因此可以并行了,只有插入同一个格子的少量并发,等待全部完成.
皮裤套棉裤,由于空间哈希网格没有外扩容机制,因此我们需要把空间网格转为四叉树一个节点.
也可以转为将空间网格转为四叉树,转换方式并不是节点交换,而查询方式通过if(node.IsGrid)跳转到网格的选择模式.
空间网格查询时候只需要折半.
有序点集
为了SIMD这碗醋包的一碗饺子,使它能够使用并行/SIMD/CPU预读/跳跃索引/减少内存OOM和碎片.
我要设计一个点集,这个点集能够容纳上百亿的点,它不能单纯用一个数组完成,因为数组在扩容的时候是采取全量复制,此时内存OOM了,因此需要用节点断开,并且节点内保证一定连续性不分裂.
选择跳表+三值数组(XYZ),似乎不错的选择.跳表是通过数组头实现有序,节点则是数组内经过插入实现有序.
注意,一旦加入了容器就是不能修改的,否则岂不是无序了,这种叫只读容器Immutable
搜索
我先说它怎么用先,想要获取矩形范围,那么跳表的索引就能快速找到这个xmin和xmax范围的节点,在这些节点内搜索ymin和ymax,此时你会发现Y字段没有索引,因此得并行SIMD进行穷举搜索.
为了并行计算,每个节点都允许一个线程进入,所以要有读写锁实现线程安全的跳表,然后节点内用SIMD,例如大规模平移/旋转等矩阵变换,或者搜索Y和Z,因为这两字段无序.
用xArray区间记录为例子[1,100][201,250]...跳表是只会记录头元素作为hashcode[1,201...]这样的形式很方便可以通过节点hashcode判断是否存在这个点,从而获取区间.
它是非平衡的,内存友好的,没有过多的节点旋转,但是怎么能设计出中间是断开的呢?
节点满了1024之后怎么拒绝?其实不是拒绝,而是分裂,新建一个key,然后移动一半过去,也就是跳表是接受有序数组作为参数输入,然后能够自动分裂这个数据(保证有序的检验参数,似乎仍然需要SIMD去实现).
插入
A:先插入1,再插入100,再插入1-100之间...再201,发现前面区间是间距太大100-1=99<201-100=101,并且数组满75%(泊松分布)就独立一个节点(直接在short).视为中间大概率存在一个空节点[101,200],即使不存在也不要紧,跳表也不记录,实现跳表版稀疏数组.
B:阈值数组长度1024,满员75%,直接在插入时候分裂一半出去新节点.
缩容
因为是多线程并行,所以我们不能在读时候锁,否则最快的读取功能就没有了.
A:删除时候,如果当前节点只有25%,并且前面容量有25%,我们就向前合并.
B:时间也是一个美好的法则,异步只读需要整理的,并且每2秒整理一次.在并行读取时候可以停止整理,跟GC似的.
C:内存不足,因为win允许虚拟内存和硬盘Swap,此处没有实验.通常来说不足时候,序列化跳表末尾成员到磁盘,并且在跳表标记省略部分,需要时候再读取磁盘.
结构
struct Node{
double Start,
double End,
int Count,
double[1024] XArray,
double[1024] YArray,
double[1024] ZArray,
bool 满足75%=>Count>1024*0.75=768
}
c#自带没有线程安全有序哈希结构的,没有类似java的跳表结构ConcurrentSkipListMap,因此需要自己拷贝一个
https://github.com/pknam/skiplist/blob/master/SkipList/ConcurrentSkipListMap.cs
参考
SIMD的三角函数,以此实现并行矩阵旋转
https://blog.ladeak.net/posts/math-sin-simd
MapReduce并行
如何在10万个图元中获取全部首尾相连?
如何在10亿个点找到邻近的点?
获取100G txt内重复度前十的名字.
单机获取十亿行(约13G)天文台数据的最低温度,最高温度,平均温度.
这都是一些大数据编程问题,要保证短时间内获取,并且还必须要全部遍历一次,而解决起来就需要各种优化技术了.
首先我们要确定硬件的能力,
衡量CPU处理能力我们会使用每秒浮点运算次数
FLOP/S(Floating Point Operations Per Secon)
1 TOP/S==1万亿FLOP/S
因此,现在的CPU是非常快的,
如果你的程序没有写到这个极限,那么说明还有优化空间.
在cad上面,我们不需要极度优化,我们要工作流级的优化就好了,
但是降低到2秒内是必要的.
我们仍然需要知道要优化什么方向.
例如时间复杂度的计算是非常重要的,数数代码里面有几个for,每个for的复杂度是多少,这是基础技能.
因为第一次山人这么数给我,我就惊了,居然时间也是能数出来的,甚至都不需要测试就知道规模了,然后你就会发现代码越长反而运行越快.
10w图元链选:
10%碰撞率
http://bbs.mjtd.com/thread-190866-1-1.html
链接里面展示了一个四叉树DFS算法.
会发现一次链选是2ms(自带的ssget60ms).
那么10w都链选一次,岂不是200秒!!
那100w个呢?时间上面太可怕了吧!!
而且我发现大家还是太迷信了,觉得四叉树能够完成这个任务,有的觉得数据库就快,殊不知数据库就是八叉树...
不要迷信数据结构好吗?
简单的处理:
剔除状态机:把foreach改为for.
减少栈帧:属性改字段/使用内联特性.
拒绝递归:四(八)叉树速度并没有特别快,不要迷恋它们了!!
一条线两个端点,都链选一次,那么时间复杂度为2n*log4(n),并且每次进入多叉树节点是递归进去的,会频繁开辟栈帧.
cad自带的ssget更甚,会进入屏幕缓冲区再进入八叉树更慢了,所以lisp遇到此问题举步维艰...
根据选择的时间复杂度分析,发现它其实不慢,只是遇到这个数量级问题上面还是太高了,会随着规模变大而亚线性增长,是线性对数时间复杂度,
如果能变成线性或者常数时间复杂度就更好了,
因此第一个优化方向需要降低这个2nlog4(n).
完成后的时间复杂度:
平均:O((nlog(n) +m) / parallel),
(快排+线性搜索)/并行.
最坏:O(n^2+n)
聪明的小子们看到这个快排就大概能明白个七七八八了,接下来听我娓娓道来.
首先明确一些优化方向:
1,链条必然出现在碰撞的地方,因此求碰撞是很快的,是粗过滤,不然还加速个毛线啊.
2,两点交错是细比较,是慢速,而且比较时候要容差判断.因为碰撞区内部也是有序的,所以两点交错比较其实可以前后向中间夹逼进行,因为CPU有分支流水线技术,所以这居然是加速的...这甚至不需要用到SIMD...
3,为什么用有序数组代替四叉树?因为不需要递归入栈,且少产生cache miss.
4,有序数组可以线性搜索.这意思就是构造有序数组(索引)无论如何都要遍历一次,然后加上线性搜索的1.x次,总体也就是2.x次,收益巨大.
5,并行化
SAP算法:
0x01
读取图元包围盒转为矩形.
矩形是4个值:左右上下,不是4个点,不是左下宽高.
*重点在于搜索速度,而不是构造速度.高频读取时候需要考虑内存瓶颈.
*矩形的数值类型通过计算db范围再自适应double,float,定点数,或者把超出范围的全部映射到定点数区域.
*其实也不用那么变态,double就算了,毕竟要极限一点还得改成SOA结构.
普通AOS就好了:调用时候List
class CadEntity:Rect { //包围盒
ObjectId Id;//当前图元
List
}
SOA结构:(排序时注意联动移动,代码非常多)
class CadEntity {
public List
public List
public List
public List
public List
public List<List
}
0x02
进行包围盒xyz排序(快排nlogn),成为只读数组.
0x03
因为已经排序,碰撞总是前面碰撞后面的,存在一个顺序窗口搜索区,此时就是线性速度求邻近碰撞.
线性有很多好处,大大贴合CPU预读数据的能力,到这步为止c#单线程74ms/c++估计能进入60ms.
3.1,粗过滤
线性碰撞就是从左下角的第一个基准图元盒A开始和它x区间的每一个图元进行矩形碰撞.实现每个图元都比较过碰撞.
x区间怎么获取?都排序了啊,A+1,A+2...图元就是了啊,超出基盒A右边范围就直接break啊.
矩形碰撞比较:
基盒A和盒N的x左比较,
如果在基盒A的x左右区间,再判断y上下区间,也在之间则为碰撞:
基盒A新建碰撞链加入盒N,盒N的链指向此链.链指针加到全局链集合,用于后续流程.
如果盒N上下穿过基盒A(此时为碰撞),
但是盒N已经被前面基盒A-视为碰撞(链指针不是null),
将基盒A和其碰撞链全部成员加入对方碰撞链,并且指针指向对方碰撞链.(选择数量少的那方并归就好了)
因此会成为无序链堆,而不是有序链条.
3.2,细比较
两点交错比较示意:要容差
LineA.P1==LineB.P1 || LineA.P1==LineB.P2 ||
LineA.P2==LineB.P1 || LineA.P2==LineB.P2
并行每个链堆进行两点交错比较,生成多段线数据.
32A:如果碰撞了直接求两点交错,此时读取图元信息是首次访问,将产生cache miss,不过却减少了储存无用碰撞区.
32B:如果记录碰撞区后再并行,因为储存数据是存入主存而不是CPU缓存,也存在线程内cache miss.
需要测试上述,A写不如算,B更符合流程.
0x04
你会发现0x01,0x02这两步是没有并行的,并且包围盒数组也不需要全局排序,因为排序是非常慢的,快排也是O(n*log(n)),对于大规模也崩坏.
最终时间=构造索引(有序数组)+选择数据.
选择数据:10w图元碰撞是74ms+两点成链时间,但是我想做到10-20ms.
4.1,因此构造有序数组(索引)前,在外面套一层并发空间哈希网格,从源头上实现任务的划分,尽可能并行+并行+并行...
但是存在大图元跨越分区,要化解这个问题.
41A:包围盒上下左右都在,才属于这个网格.
41B:出现跨网格大包围盒,
每个子网格都要有一个跨网格容器(实际上是全局的map),
获取大包围盒左下角子网格的跨网格容器,
判断容器矩形范围:
相同就加入图元.
不同就通过大包围盒矩形新建大网格,大网格加入图元,并加入跨网格容器.
41C:简单的来说,大网格图元就是和全部子网格图元碰撞,而细比较时候,无可避免要遍历子网格全部图元,但是并行子网格排序的时间收益更高.
大网格利用已经排好序的子网格只读数组,进行线性求细比较.
*错误几点:
*把大包围盒id都加入面积区全部子网格?类似栅格化,可以减少子网格内容碰撞,但是网格细分出现多个网格重复添加id,储存到集合总是特别慢的.
*如果将排序后的包围盒数组/CPU核心数,对于每域并行化求碰撞,不过这种切割方式不合理,存在跨域碰撞,难以找到最后一组x的邻近碰撞.还是得是成碰撞区后并行碰撞区.
十亿点排序
上面0x04 4.1可能大家没做之前难以感受,通过此例子也可以感受感受.
我们把上面的包围盒集想像成一个点集先,这样我们就扩展到了十亿点集求最近距离问题了.
var sortedPoints = points
.OrderBy(p => p.X).ThenBy(p => p.Y).ThenBy(p => p.Z);
排序之后的点集,同x就是一组(纵向),最近点必然出现在同组(上下)或者邻近组(左右),如果有Z还有前后.
排序前,需要一定的容差.可以向下取整(推荐),可以制作有序数组,二分法求邻近组值,接近则加入其组.
一次性十亿个点排序?你要疯了哦.
必须要先用空间哈希网格去切割点集.
如果那么刚好切割到某个邻近点到下一个分区,此时就尴尬了,边缘还得去邻近一圈网格搜索,
四周一圈邻近网格如果是空的,还得去下一圈网格,
这个就是读扩散问题.
但是,读扩散非常少,而排序总是极慢,那么多线程并行排序多个网格就显得更快.
网格越多,并行化越高,缓存命中越高,读扩散风险越高.
所以通篇下来,你会发现就是MapReduce思想:
先分区,并行分区的任务,最后合并.
(完)