打开图层如何从耗时20秒优化到2秒

引文

测试中发现,一个17M的文件打开时间超过了20秒。追踪发现,这个文件包含一个图层,图层有10多万个对象。为了进行对比,导出dwg,用AutoCAD打开,发现耗时不到3秒钟。看起来这个优化的空间很大!

优化加载图层

打印日志,统计时间看出加载数据集耗时1秒,但是加载图层耗费了近9秒。按理说,两个记录数一致的两张表,为何会差距如此之大?

查询语句不同

图层的查询与数据集最大的不同在于使用了order by关键字,按照level排序检索。用来调整对象的显示顺序。当在这个字段上补充上索引之后,加载图层时间立即降到了4秒。
经验总结:当查询语句包含order by时,在相应的字段补充索引。

风格集

图层的查询第二个特色便是:加载时通过维护一个风格集合哈希表来减少风格的内存占用,十几万个对象可能使用的风格在2000个以内甚至更少。但是每一条记录都需要进行哈希表的查询,这会增加加载的时间。而哈希表键值越大,比较的内容越多,耗时越长。类似于线宽的键值,用float代替double可以满足基本需求,但是键值占据空间减少了一半,比较的时间也就越短了。修改之后加载图层耗时842毫秒,已经与加载数据集不相上下了。
下图是加载图层优化过程计时。
image

优化解析元胞

经过图层加载优化之后,整个图层打开耗时从21秒下降到了13秒,算是一个比较大的提升了。但是与CAD(3秒)相比,还有很大的差距。需要做更深层次的优化。
在这个图层中,10几万个对象都是多段线,而多段线是由圆弧组成的。在解析元胞时执行了一个非常重要的函数,getCoordinates,从圆弧得到拟合点,最后以线段输出。
原始代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
LrUInt32 GeoCircularArc::getCoordinates(std::vector<Coor>& reCos, LrUInt8 flag/* = ESplineFit_Null*/) const
{
LrDouble endAng = 0;
LrDouble startAng = 0;
if (_arc._bClosed)
{
// 闭合
startAng = 0;
endAng = 360;
}
else
{
// 依据顺逆调整起终角
endAng = MathUtility::normal_angle(_arc._endAngle);
startAng = MathUtility::normal_angle(_arc._startAngle);

if(!_arc._bCounterClockwise)
{
std::swap(startAng, endAng);
}
}

LrUInt32 nCount = 0;
std::vector<Coor> tempCos;
if (endAng < startAng)
{
while(endAng < startAng)
{
endAng += 360.0;
}
}

LrDouble current = startAng;
Coor co;
//
LrDouble arcStep = ConfigUtility::instance()->getDouble(ArcStep);
if (arcStep < MathUtility::LrFloat_Epsilon)
{
arcStep = 1.0;
}

LrBool manualClip = ConfigUtility::instance()->getBool(ManualClip);
if (manualClip && arcStep < 5.)
{// 手动裁剪加大间隔,防止多边形计算出错
arcStep = 5.;
}

while (current < endAng)
{
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(current));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(current));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
current += arcStep;

nCount++;
}

// 补充最后一点
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(endAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(endAng));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
nCount++;

// 依据顺逆调整点序
if(_arc._bCounterClockwise)
{
reCos.insert(reCos.end(),tempCos.begin(),tempCos.end());
}
else
{
reCos.insert(reCos.end(),tempCos.rbegin(),tempCos.rend());
}

return nCount;
}

全局变量查找优化

函数依据全局配置,获取间隔角度,然后依据此角度计算拟合点。其中:

1
ConfigUtility::instance()->getDouble(ArcStep);

访问了一个map表,从其中获取全局变量。map的效率是很高的,但是在重复访问10w次时,效率当然比不上直接访问数值。所以果断改为:

1
ConfigUtility::instance()->getArcStep();

耗时从11秒降到10秒3,降低幅度虽然有限,但是提升了效率,修改之后代码如下(注意注释掉的代码作为对比):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
LrUInt32 GeoCircularArc::getCoordinates(std::vector<Coor>& reCos, LrUInt8 flag/* = ESplineFit_Null*/) const
{
LrDouble endAng = 0;
LrDouble startAng = 0;
if (_arc._bClosed)
{
// 闭合
startAng = 0;
endAng = 360;
}
else
{
// 依据顺逆调整起终角
endAng = MathUtility::normal_angle(_arc._endAngle);
startAng = MathUtility::normal_angle(_arc._startAngle);

if(!_arc._bCounterClockwise)
{
std::swap(startAng, endAng);
}
}

LrUInt32 nCount = 0;
std::vector<Coor> tempCos;
if (endAng < startAng)
{
while(endAng < startAng)
{
endAng += 360.0;
}
}

LrDouble current = startAng;
Coor co;
//
/*
double arcStep = ConfigUtility::instance()->getDouble(ArcStep);
if (arcStep < MathUtility::LrFloat_Epsilon)
{
arcStep = 1.0;
}

LrBool manualClip = ConfigUtility::instance()->getBool(ManualClip);
if (manualClip && arcStep < 5.)
{// 手动裁剪加大间隔,防止多边形计算出错
arcStep = 5.;
}
*/
LrDouble arcStep = ConfigUtility::instance()->getArcStep();
if (arcStep < MathUtility::LrFloat_Epsilon)
{
arcStep = 1.;
}

if (ConfigUtility::instance()->getManualClip())
{
arcStep = 5.;
}

while (current < endAng)
{
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(current));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(current));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
current += arcStep;

nCount++;
}

// 补充最后一点
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(endAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(endAng));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
nCount++;

// 依据顺逆调整点序
if(_arc._bCounterClockwise)
{
reCos.insert(reCos.end(),tempCos.begin(),tempCos.end());
}
else
{
reCos.insert(reCos.end(),tempCos.rbegin(),tempCos.rend());
}

return nCount;
}

坐标调序

函数的最后,依据圆弧顺逆时针插入坐标数组的顺序有差别。当圆弧是顺时针,将tempCos插入reCos时,顺序与逆时针是相反的。也就是说,这里需要有一个tempCos的取反操作。这样写代码简单,但是逆序操作比较耗时,我们可以手动控制插入的顺序,修改后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
LrUInt32 GeoCircularArc::getCoordinates(std::vector<Coor>& reCos, LrUInt8 flag/* = ESplineFit_Null*/) const
{
LrDouble endAng = 0;
LrDouble startAng = 0;
if (_arc._bClosed)
{
// 闭合
startAng = 0;
endAng = 360;
}
else
{
// 依据顺逆调整起终角
endAng = MathUtility::normal_angle(_arc._endAngle);
startAng = MathUtility::normal_angle(_arc._startAngle);

if(!_arc._bCounterClockwise)
{
std::swap(startAng, endAng);
}
}

LrUInt32 nCount = 0;
std::vector<Coor> tempCos;
if (endAng < startAng)
{
while(endAng < startAng)
{
endAng += 360.0;
}
}

LrDouble current = startAng;
Coor co;
//
/*
double arcStep = ConfigUtility::instance()->getDouble(ArcStep);
if (arcStep < MathUtility::LrFloat_Epsilon)
{
arcStep = 1.0;
}

LrBool manualClip = ConfigUtility::instance()->getBool(ManualClip);
if (manualClip && arcStep < 5.)
{// 手动裁剪加大间隔,防止多边形计算出错
arcStep = 5.;
}
*/
LrDouble arcStep = ConfigUtility::instance()->getArcStep();
if (arcStep < MathUtility::LrFloat_Epsilon)
{
arcStep = 1.;
}

if (ConfigUtility::instance()->getManualClip())
{
arcStep = 5.;
}

if (_arc._bCounterClockwise)
{
while (current < endAng)
{
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(current));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(current));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
current += arcStep;

nCount++;
}

// 补充最后一点
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(endAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(endAng));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
nCount++;
}
else
{
current = endAng;
while (current > startAng)
{
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(current));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(current));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
current -= arcStep;

nCount++;
}

// 补充最后一点
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(startAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(startAng));
co.z = _arc._circle.co.z;
tempCos.push_back(co);
nCount++;
}

/*
// 依据顺逆调整点序
if(_arc._bCounterClockwise)
{
reCos.insert(reCos.end(),tempCos.begin(),tempCos.end());
}
else
{
reCos.insert(reCos.end(),tempCos.rbegin(),tempCos.rend());
}
*/
return nCount;
}

代码量比刚才多了一些,但是耗时从10秒3降到了7秒2,优化了3秒钟!!!

三角函数结果存储

不难发现,这个函数中用得最多的三角函数运算,cos&sin。而平台最小角度为1°,我们完全可以预先把360度的cos&sin预先计算出来,存储到一个数组中,在使用时直接从数组读取,减少计算的时间。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
LrUInt32 GeoCircularArc::getCoordinates(std::vector<Coor>& reCos, LrUInt8 flag/* = ESplineFit_Null*/) const
{
LrDouble endAng = 0;
LrDouble startAng = 0;
if (_arc._bClosed)
{
// 闭合
startAng = 0;
endAng = 360;
}
else
{
// 依据顺逆调整起终角
endAng = MathUtility::normal_angle(_arc._endAngle);
startAng = MathUtility::normal_angle(_arc._startAngle);

if(!_arc._bCounterClockwise)
{
std::swap(startAng, endAng);
}
}

LrUInt32 nCount = 0;
//std::vector<Coor> tempCos;
if (endAng < startAng)
{
while(endAng < startAng)
{
endAng += 360.0;
}
}

Coor co;
//
/*
double arcStep = ConfigUtility::instance()->getDouble(ArcStep);
if (arcStep < MathUtility::LrFloat_Epsilon)
{
arcStep = 1.0;
}

LrBool manualClip = ConfigUtility::instance()->getBool(ManualClip);
if (manualClip && arcStep < 5.)
{// 手动裁剪加大间隔,防止多边形计算出错
arcStep = 5.;
}
*/
LrDouble arcStep = ConfigUtility::instance()->getArcStep();
if (arcStep < MathUtility::LrFloat_Epsilon)
{
arcStep = 1.;
}

if (ConfigUtility::instance()->getManualClip())
{
arcStep = 5.;
}

static LrBool tag = false;
static LrDouble cosArray[720] = { 0 };
static LrDouble sinArray[720] = { 0 };
if (!tag)
{
tag = true;
for (size_t i = 0;i < 720; ++i)
{
cosArray[i] = std::cos(MathUtility::angle_to_arcangle(i));
sinArray[i] = std::sin(MathUtility::angle_to_arcangle(i));
}
}

int step = arcStep;
if (_arc._bCounterClockwise)
{
int current = startAng;
if (startAng > current)
{
// 非整数处理
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(startAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(startAng));
co.z = _arc._circle.co.z;
reCos.push_back(co);
current += step;
nCount++;
}

while (current < endAng)
{
int index = (int)current;
co.x = _arc._circle.co.x + _arc._circle.r * cosArray[current];
co.y = _arc._circle.co.y + _arc._circle.r * sinArray[current];
co.z = _arc._circle.co.z;
reCos.push_back(co);
current += step;
nCount++;
}

// 补充最后一点
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(endAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(endAng));
co.z = _arc._circle.co.z;
reCos.push_back(co);
nCount++;
}
else
{
int current = endAng;
if (endAng > current)
{
// 非整数处理
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(endAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(endAng));
co.z = _arc._circle.co.z;
reCos.push_back(co);
//current -= step;
nCount++;
}

while (current > startAng)
{
co.x = _arc._circle.co.x + _arc._circle.r * cosArray[current];
co.y = _arc._circle.co.y + _arc._circle.r * sinArray[current];
co.z = _arc._circle.co.z;
reCos.push_back(co);
current -= step;
nCount++;
}

// 补充最后一点
co.x = _arc._circle.co.x + _arc._circle.r * std::cos(MathUtility::angle_to_arcangle(startAng));
co.y = _arc._circle.co.y + _arc._circle.r * std::sin(MathUtility::angle_to_arcangle(startAng));
co.z = _arc._circle.co.z;
reCos.push_back(co);
nCount++;
}

/*
// 依据顺逆调整点序
if(_arc._bCounterClockwise)
{
reCos.insert(reCos.end(),tempCos.begin(),tempCos.end());
}
else
{
reCos.insert(reCos.end(),tempCos.rbegin(),tempCos.rend());
}
*/
return nCount;
}

这里之所以用720,是为了让类似于起始角度270、终止角度90、逆时针圆弧计算更加便捷。
这个优化,让解析的时间从7.2秒降低到6秒,提升空间也不是很大,但是也提供了一种思路。
下图是解析元胞优化过程计时图。
image
到此,打开图层从20秒降低到了8秒,时间减少了60%,算是比较大的提升了。
但是为什么距离cad还有这么大的距离?
我们每隔1度取一个点,是不是有点多了?尝试着将角度改为15、60,测试结果如下:
image
很显然,当间隔增大时,优化结果极其明显,当间隔角度为15度时,解析元胞只需要995毫米,当间隔角度为60时,耗时695毫秒!此时图层打开时间2652毫米,已经与cad接近了!
但是当间隔角度60,我们打开图形,看到的是一个个六边形,不是圆或者圆弧。

圆弧元胞

从显示需求来看,每间隔1°获取拟合点也不是一个合理的行为。在圆弧很小时,360个点显得非常的多余,当放大时360个点可能还不够用。
windows底层提供了绘制圆弧的API,我们可以尝试不再获取圆弧拟合点,只给出圆心、起始、终止的基础数据,让绘制引擎去决定这个圆弧最终如何显示吧!
增加圆弧元胞之后对比如下:
image
很明显,解析元胞时间进一步缩短为423毫秒,打开图层总时间2秒,降低了90%!(从21到2)。
占用内存与角度间隔60相差无几,比1度间隔降低了70%。而CAD打开相同文件占据内存0.8G。虽然时间上还落后与CAD,但是内存占用上这个文件我们胜出了。
即:本次优化空间和时间维度都得到了很大的性能提升!

可能有的问题和进一步优化

可能的问题

唯一可能担心的是,windows的圆弧绘制在视图放大到一定程度无法显示了(半径太大)。
解决:当视图放大到半径太大时,说明视图区域可以显示的圆弧为极少数了,我们可以在这个时候再进行上面的getCoordinates,手动获取拟合点。
这个函数经过优化之后,在这里正好能够再次派上用场!

进一步可能的优化

打开整个文件耗时在3.1~3.5左右,而CAD则基本在3秒以内。
每一个数据集和图层数据是相互独立的,或许可以开启多个线程加载多个图层。