近期在做一个系统时发现项目中用的A*算法效率太差了,具体要时间上的话就是一个100路径点的寻路要花上大概50ms左右,在需要大量寻路的时候会非常卡。
1、首先分析A*算法的过程,摘自百度百科http://baike.baidu.com/view/7850.htm?fr=aladdin
while
(OPEN!=NULL)
{
从OPEN表中取估价值f(n)最小的节点n;
if
(n节点==目标节点)
break
;
for
(当前节点n的每个子节点X)
{
算X的估价值;
if
(XinOPEN)
if
(X的估价值小于OPEN表的估价值)
{
把n设置为X的父亲;
更新OPEN表中的估价值;
//取最小路径的估价值
}
if
(XinCLOSE)
continue
;
if
(Xnotinboth)
{
把n设置为X的父亲;
求X的估价值;
并将X插入OPEN表中;
//还没有排序
}
}
//endfor
将n节点插入CLOSE表中;
按照估价值将OPEN表中的节点排序;
//实际上是比较OPEN表内节点f的大小,从最小路径的节点向下进行。
}
//endwhile(OPEN!=NULL)
2、启用fprof,查看各个操作具体耗时
fprof_start(Procs) ->
[fprof:trace([start, {file, "/tmp/fprof.trace"}, {procs, Proc}]) || Proc <- Procs].
fprof_stop() ->
Now = fun() ->
{A, B, _} = erlang:now(),
A * 1000000 + B
end,
ok = fprof:trace(stop),
ok = fprof:profile({file, "/tmp/fprof.trace"}),
Analyse = lists:concat(["/tmp/fprof-", Now(), ".analysis"]),
ok = fprof:analyse([{dest, Analyse}, {details, true}, {totals, true}, {sort, own}]),
ok.
上述方法就是一次fprof启动和终止过程,对于过程内的每一次调用都会记录到指定的文件中,具体可以查看手册中关于fprof的用法。
3、查看生成的文件内容,找出耗时的具体函数
% CNT ACC OWN
[{ totals, 28740,31024.155, 181.700}]. %%%
{[{{aStar,is_tile_walkable,3}, 581, 113.807, 65.051},
{{lists,member,2}, 458, 0.000, 42.260}],
{ {lists,member,2}, 1039, 113.807, 107.311}, %
[{{lists,member,2}, 458, 0.000, 42.260},
{suspend, 458, 6.496, 0.000}]}.
{[{{lists,foldl,3}, 3810, 0.000, 9.036},
{{aStar,insert_open_list,1}, 327, 15.254, 0.656},
{{erl_lint,bool_option,4}, 26, 0.185, 0.053},
{{orddict,from_list,1}, 4, 0.119, 0.024},
{{erl_eval,merge_bindings,2}, 7, 0.133, 0.014},
{{dict,from_list,1}, 6, 3.200, 0.012},
{{erl_lint,value_option,7}, 2, 0.014, 0.004},
{{erl_lint,exprs_opt,3}, 2, 0.020, 0.004},
{{erl_lint,expr_list,3}, 2, 0.042, 0.003}],
从上面的内容可以看出,一次操作中,aStar:is_tile_walkable这个方法调用次数最多,占用时间也是最多,其中最主要的消耗在于lists:member。出现上述这种情况是因为,对于算法中需要探测的每一个点,都要检测其周围的点是否是可走的点,如果是可走的点才会加入openlist中。判断一个点是否可走的方法是,查看该点是否在不可走点的列表中。不可走点的列表是{X, Y}类数据的tuplelist。
4、针对lists:member这个最耗时操作优化
由于函数lists:member(Item, List)是依次用List中的每一个元素去检查是否匹配Item,所以对于{X, Y}这种结构的匹配比一个单独X要复杂,所以先把不可走点列表中的{X, Y}结构转换为X,具体转换规则可以自由定制。这样改完之后,原本50ms的时间减小到了30ms。
5、进一步优化
30ms对于一些情况下还是太过耗时了,而整个算法最耗时操作在lists:member,这种操作的时间复杂度是o(n),如果能改结构呢?用二叉树存储不可走点列表,那么理论上复杂度就是o(logn)。修改结构,使用gb_sets存储,再次检查时间消耗只有2ms。
结论:1、list是erlang中常用的结构,erlang很多操作是通过遍历list实现的,但是如果在数据量上升到100以上,效率会下降很多。2、erlang中自带的二叉树结构有gb_sets,gb_trees,对于处理大量数据的查找、插入很有效。3、fprof这个工具在分析性能上非常好用,结合timer:tc可以很快找到问题。
附:A*算法代码
%% Author: pyf
%% Created: 2014-8-3
%% Description: A*算法
-module(aStar).
%%
%% Include files
%%
-include("logger.hrl").
-include("mapDefine.hrl").
-record(r_map_node, {key, g, f, p_parent}).
-define(close_list_length, 400).
%%
%% Exported Functions
%%
-export([find_path/4,
find_path/5]).
%%
%% API Functions
%%
%%Astar算法寻找路径,地图相关进程调用
find_path(StartX, StartY, EndX, EndY) ->
case StartX =:= EndX andalso StartY =:= EndY of
true ->
[{StartX, StartY}];
false ->
UnWalkAbleTile = make_unwalkable_tile(),
find_path2(StartX, StartY, EndX, EndY, UnWalkAbleTile)
end.
find_path(MapDataID, StartX, StartY, EndX, EndY) ->
case StartX =:= EndX andalso StartY =:= EndY of
true ->
[{StartX, StartY}];
false ->
UnWalkAbleTile = make_unwalkable_tile(MapDataID),
find_path2(StartX, StartY, EndX, EndY, UnWalkAbleTile)
end.
%%初始化起始点StartNode,初始化openlist和closelist
find_path2(StartX, StartY, EndX, EndY, UnWalkAbleTile) ->
StartNode = #r_map_node{key = {StartX, StartY}, g = 0},
set_open_list([]),
set_close_list([]),
find_path3(EndX, EndY, StartNode, UnWalkAbleTile).
find_path3(EndX, EndY, CurNode, UnWakeAbleTile) ->
case catch insert_aound_map_nodes(CurNode, EndX, EndY, UnWakeAbleTile) of
{ok, get_it} ->
Path = [{EndX, EndY}],
find_path4(CurNode, Path);
Bool when is_atom(Bool) ->
[MinNode | T] = get_open_list(),
set_open_list(T),
Bool andalso insert_close_list(CurNode, get_close_list()),
find_path3(EndX, EndY, MinNode, UnWakeAbleTile);
{failed} ->
[];
Other ->
?ERR(astar, "~w", [Other])
end.
find_path4(#r_map_node{p_parent = undefined} = MapNode, Path) ->
open_list_delete(),
close_list_delete(),
[MapNode#r_map_node.key | Path];
find_path4(CurNode, Path) ->
ParentNode = get_close_list(CurNode#r_map_node.p_parent),
find_path4(ParentNode, [CurNode#r_map_node.key | Path]).
%%
%% Local Functions
%%
%%构造不可走的点,为了方便测试,这里随机构造100个点,其中不包括测试起始点{0, 0}和测试终止点{100, 100}
make_unwalkable_tile() ->
mapViewConst:block2c_list().
%% F = fun(_N, AccIn) ->
%% [{random:uniform(100), random:uniform(100)} | AccIn]
%% end,
%% lists:foldl(F, [], lists:seq(1, 2000)).
make_unwalkable_tile(MapDataID) ->
case get({?MapView_PhyList2C, MapDataID}) of
undefined ->
MapView = mapManager:getMapView(MapDataID),
put(?MapView_LogicCellNumX, MapView#mapView.logicCellNumX),
F = fun(CurIndex, Elem, Acc) ->
case Elem =:= true of
true ->
[CurIndex | Acc];
false ->
Acc
end
end,
TileList = array:foldl(F, [], MapView#mapView.phyArray),
GBTileList = gb_sets:from_list(TileList),
put({?MapView_PhyList2C, MapDataID}, GBTileList),
GBTileList;
TileList ->
TileList
end.
%%计算从点{X, Y}到{X1, Y1}的期望值
get_qi_wang_zhi(X, Y, X1, Y1) ->
erlang:abs(X1 - X) + erlang:abs(Y1 - Y).
%%插入元素Element到closelist中
insert_close_list(Element, CloseList) ->
case length(CloseList) >= ?close_list_length of
true ->
throw({failed});
false ->
set_close_list([Element | CloseList])
end.
%%更新openlist
update_open_list(MapNode) ->
OpenList = get_open_list(),
F = fun(#r_map_node{key = Key}, {AccInH, AccInT}) ->
[AccInTH | AccInTT] = AccInT,
case Key =:= MapNode#r_map_node.key of
true ->
throw({get, lists:reverse([MapNode | AccInH], AccInTT)});
false ->
{[AccInTH | AccInH], AccInTT}
end
end,
case catch lists:foldl(F, {[], OpenList}, OpenList) of
{get, NewOpenList} ->
set_open_list(NewOpenList);
_ ->
set_open_list(OpenList)
end.
insert_open_list(MapNode) ->
OpenList = get_open_list(),
F = fun(#r_map_node{f = F}, {AccInH, AccInT}) ->
case F > MapNode#r_map_node.f of
true ->
throw({get, lists:reverse([MapNode | AccInH], AccInT)});
false ->
[AccInTH | AccInTT] = AccInT,
{[AccInTH | AccInH], AccInTT}
end
end,
case catch lists:foldl(F, {[], OpenList}, OpenList) of
{get, NewOpenList} ->
set_open_list(NewOpenList);
_ ->
set_open_list(OpenList ++ [MapNode])
end.
%%判断点是否可走
is_tile_walkable(TX, TY, UnWakeAbleTile) ->
%% not lists:member({TX, TY}, UnWakeAbleTile).
NumX = get(?MapView_LogicCellNumX),
Index = TY * NumX + TX,
TX >= 0 andalso TY >= 0 andalso gb_sets:is_element(Index, UnWakeAbleTile) =/= true.
%%检查点{TX, TY}是否closelist中的元素
close_list_member({TX, TY}) ->
CloseList = get_close_list(),
lists:keyfind({TX, TY}, #r_map_node.key, CloseList) =/= false.
%%遍历当前点CurNode周围的每一个点{TX, TY},并且对将其插入到openlist或者更新openlist
insert_aound_map_nodes(CurNode, EndTX, EndTY, UnWakeAbleTile) ->
#r_map_node{key = {CTX, CTY}} = CurNode,
F = fun({TXAdd, TYAdd}, AccIn) ->
TX = CTX + TXAdd,
TY = CTY + TYAdd,
if TX =:= EndTX andalso TY =:= EndTY ->
erlang:throw({ok, get_it});
TX =:= CTX andalso TY =:= CTY ->
ignore;
true ->
insert_aound_map_nodes(CurNode, EndTX, EndTY, TX, TY, close_list_member({TX, TY}), UnWakeAbleTile) orelse AccIn
end
end,
lists:foldl(F, false, [{0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}, {-1, 0}, {-1, 1}]).
insert_aound_map_nodes(_CurNode, _EndTX, _EndTY, _TX, _TY, true, _UnWakeAbleTile) ->
false;
insert_aound_map_nodes(CurNode, EndTX, EndTY, TX, TY, false, UnWakeAbleTile) ->
OpenList = get_open_list(),
MapNode = new_map_node(TX, TY, CurNode, EndTX, EndTY),
case lists:keyfind({TX, TY}, #r_map_node.key, OpenList) of
false ->
case is_tile_walkable(TX, TY, UnWakeAbleTile) of
true ->
insert_open_list(MapNode),
true;
false ->
false
end;
OldMapNode ->
%%如果新的路径点MapNode已经是openlist中的元素OldMapNode,则比较两个路径的估值f,openlist中保留f小的那个
case MapNode#r_map_node.f >= OldMapNode#r_map_node.f of
true ->
false;
false ->
update_open_list(MapNode),
true
end
end.
%%构造一个新的路径点
new_map_node(TX, TY, CurNode, EndTX, EndTY) ->
G = CurNode#r_map_node.g + 1,
H = get_qi_wang_zhi(TX, TY, EndTX, EndTY),
#r_map_node{key={TX, TY}, g = G, f = G + H, p_parent = CurNode#r_map_node.key}.
get_open_list() ->
erlang:get(astar_open_list).
set_open_list(OpenList) ->
erlang:put(astar_open_list, OpenList).
open_list_delete() ->
erlang:erase(astar_open_list).
get_close_list() ->
erlang:get(astar_close_list).
get_close_list({TX, TY}) ->
lists:keyfind({TX, TY},#r_map_node.key, get_close_list()).
set_close_list(CloseList) ->
erlang:put(astar_close_list, CloseList).
close_list_delete() ->
erlang:erase(astar_close_list).