From c2628d43b72d22ca4c363defc8055733e09fa708 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Fri, 18 Feb 2011 20:08:26 +0100 Subject: [PATCH 01/12] Clean up A* sector cache leftovers The sector cache was disabled in v4.2.2, and dropped in commit 8f40f5ad, v4.2.20. A bit of cache statistics code was left behind. Remove it. --- src/lib/common/path.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/lib/common/path.c b/src/lib/common/path.c index 6313a617..cbd5f0a6 100644 --- a/src/lib/common/path.c +++ b/src/lib/common/path.c @@ -51,8 +51,6 @@ #define BP_NEIGHBORS 6 /* max number of neighbors */ struct bestp { - int sctcache_hits; - int sctcache_misses; int bp_mobtype; struct as_data *adp; }; @@ -123,11 +121,6 @@ best_path(struct sctstr *from, struct sctstr *to, char *path, int mob_type) #ifdef AS_STATS as_stats(adp, stderr); #endif /* AS_STATS */ -#ifdef BP_STATS - fprintf(stderr, "best path %s\n", path); - fprintf(stderr, "cache hits/misses: %d/%d\n", - bp->sctcache_hits, bp->sctcache_misses); -#endif /* BP_STATS */ return 0; } From 8f92fe40f44bb47e42e1e1c2f8456d1f8bb05b9b Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sat, 19 Feb 2011 10:47:32 +0100 Subject: [PATCH 02/12] More precise and complete A* performance statistics Memory usage didn't include path (adp->path), neighbor cache (adp->neighbor_coords, adp->neighbor_nodes), and the hash table (adp->hashtab). While there, print path length. To get A* statistics on stderr, compile with AS_STATS defined. --- src/lib/as/as_stats.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lib/as/as_stats.c b/src/lib/as/as_stats.c index cadc8dc4..ed8d4885 100644 --- a/src/lib/as/as_stats.c +++ b/src/lib/as/as_stats.c @@ -31,8 +31,11 @@ as_stats(struct as_data *adp, FILE * fp) int i; int j; int total_q; + int total_p; int total_h; + size_t other; struct as_queue *qp; + struct as_path *pp; struct as_hash *hp; fprintf(fp, "Statistics:\n"); @@ -52,6 +55,10 @@ as_stats(struct as_data *adp, FILE * fp) i++; fprintf(fp, "\tsubsumed:\t%d\n", i); total_q += i; + for (i = 0, pp = adp->path; pp; pp = pp->next) + i++; + total_p = i; + fprintf(fp, "path length: %d\n", total_p); fprintf(fp, "hash table statistics (size %d):\n", adp->hashsize); for (i = 0; i < adp->hashsize; i++) { for (j = 0, hp = adp->hashtab[i]; hp; hp = hp->next) @@ -64,10 +71,18 @@ as_stats(struct as_data *adp, FILE * fp) fprintf(fp, "\tqueues\t%d\n", (int)(total_q * sizeof(struct as_queue))); fprintf(fp, "\tnodes\t%d\n", (int)(total_q * sizeof(struct as_node))); + fprintf(fp, "\tpath\t%d\n", (int)(total_p * sizeof(struct as_path))); fprintf(fp, "\thash ents\t%d\n", (int)(total_h * sizeof(struct as_hash))); + other = sizeof(struct as_data); + other += adp->maxneighbors * sizeof(struct as_coord); + other += (adp->maxneighbors + 1) * sizeof(struct as_node *); + other += adp->hashsize * sizeof(struct as_hash *); + fprintf(fp, "\tother\t%d\n", (int)other); fprintf(fp, "\ttotal\t%d\n", (int)(total_q * sizeof(struct as_queue) + total_q * sizeof(struct as_node) + - total_h * sizeof(struct as_hash))); + total_p * sizeof(struct as_path) + + total_h * sizeof(struct as_hash) + + other)); } From 2797e58c20fb60a286cb016540e0dc1a190e5356 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sat, 19 Feb 2011 13:19:27 +0100 Subject: [PATCH 03/12] A* path and neighbor cache performance statistics as_clear_cachepath() now prints cache hits, misses, number of entries, and memory use to stderr, when compiled with AS_STATS defined. --- src/lib/as/as_cache.c | 42 +++++++++++++++++++++++++++++++++++++++++- src/lib/common/path.c | 2 ++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/lib/as/as_cache.c b/src/lib/as/as_cache.c index d98e088e..eff08c31 100644 --- a/src/lib/as/as_cache.c +++ b/src/lib/as/as_cache.c @@ -58,6 +58,15 @@ static struct as_frompath **fromhead = (struct as_frompath **)0; static int as_cachepath_on = 0; /* Default to off */ +#ifdef AS_STATS +unsigned as_cache_tries, as_cache_hits; +#define as_cache_try() ((void)as_cache_tries++) +#define as_cache_hit() ((void)as_cache_hits++) +#else +#define as_cache_try() ((void)0) +#define as_cache_hit() ((void)0) +#endif + void as_enable_cachepath(void) { @@ -142,6 +151,13 @@ as_clear_cachepath(void) struct as_frompath *from, *from2; struct as_topath *to, *to2; int i, j; +#ifdef AS_STATS + size_t index_sz = 0; + unsigned index_nb = 0, paths_nb = 0, paths = 0; +#define stats_index(sz) ((void)(index_sz += (sz), index_nb++)) +#else +#define stats_index(sz) ((void)0) +#endif /* Cache not used yet :) */ if (fromhead == NULL) @@ -153,22 +169,43 @@ as_clear_cachepath(void) for (to = from->tolist[i]; to; to = to2) { to2 = to->next; /* Free this path */ +#ifdef AS_STATS + { + struct as_path *pp; + for (pp = to->path; pp; pp = pp->next) + paths_nb++; + paths++; + } +#endif as_free_path(to->path); /* Free this node */ free(to); + stats_index(sizeof(*to)); } } /* Now, free the list of lists */ free(from->tolist); + stats_index(WORLD_Y * sizeof(*from->tolist)); /* Save the next pointer */ from2 = from->next; /* now, free this from node */ free(from); + stats_index(sizeof(*from)); } } /* Note we don't free the fromhead here, we just zero it. That way, we can use it next time without mallocing int */ memset(fromhead, 0, (sizeof(struct as_frompath *) * WORLD_Y)); + stats_index(WORLD_Y * sizeof(*fromhead)); +#ifdef AS_STATS + fprintf(stderr, "as_cache %u searches, %u hits, %u entries," + " index %zu bytes %u blocks, paths %zu bytes %u blocks\n", + as_cache_tries, as_cache_hits, + paths, + index_sz, index_nb, + paths_nb * sizeof(struct as_path), paths_nb); + as_cache_hits = as_cache_tries = 0; +#endif } struct as_path * @@ -177,6 +214,7 @@ as_find_cachepath(coord fx, coord fy, coord tx, coord ty) struct as_frompath *from; struct as_topath *to; + as_cache_try(); /* Is the cache on? if not, return NULL */ if (as_cachepath_on == 0) return NULL; @@ -189,8 +227,10 @@ as_find_cachepath(coord fx, coord fy, coord tx, coord ty) for (from = fromhead[fy]; from; from = from->next) { if (from->x == fx) { for (to = from->tolist[ty]; to; to = to->next) { - if (to->x == tx) + if (to->x == tx) { + as_cache_hit(); return to->path; + } } } } diff --git a/src/lib/common/path.c b/src/lib/common/path.c index cbd5f0a6..965da6e2 100644 --- a/src/lib/common/path.c +++ b/src/lib/common/path.c @@ -120,6 +120,8 @@ best_path(struct sctstr *from, struct sctstr *to, char *path, int mob_type) #ifdef AS_STATS as_stats(adp, stderr); + fprintf(stderr, "neighbor cache %zu bytes\n", + WORLD_SZ() * 6 * sizeof(struct sctstr *)); #endif /* AS_STATS */ return 0; } From 0385c67a8fce0c17c341364cc340428de08c8b45 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sat, 19 Feb 2011 13:50:10 +0100 Subject: [PATCH 04/12] Fix when best_path() prints A* performance statistics Print them when A* actually runs, not when best_path() finds a path. Statistics for unsuccessful runs were lost, and old statistics were printed for path cache hits. --- src/lib/common/path.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/common/path.c b/src/lib/common/path.c index 965da6e2..9cdf959e 100644 --- a/src/lib/common/path.c +++ b/src/lib/common/path.c @@ -98,6 +98,7 @@ best_path(struct sctstr *from, struct sctstr *to, char *path, int mob_type) static struct bestp *mybestpath; struct as_data *adp; struct as_path *ap; + int res; if (!mybestpath) mybestpath = bp_init(); @@ -110,19 +111,19 @@ best_path(struct sctstr *from, struct sctstr *to, char *path, int mob_type) adp->to.y = to->sct_y; mybestpath->bp_mobtype = mob_type; - if (as_search(adp) < 0) + res = as_search(adp); +#ifdef AS_STATS + as_stats(adp, stderr); + fprintf(stderr, "neighbor cache %zu bytes\n", + WORLD_SZ() * 6 * sizeof(struct sctstr *)); +#endif + if (res < 0) return -1; ap = adp->path; } if (bp_path(ap, path) < 0) return -1; - -#ifdef AS_STATS - as_stats(adp, stderr); - fprintf(stderr, "neighbor cache %zu bytes\n", - WORLD_SZ() * 6 * sizeof(struct sctstr *)); -#endif /* AS_STATS */ return 0; } From a02d3e9fc17b17af720f60c5dbd0639c8858481d Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sat, 19 Feb 2011 15:00:03 +0100 Subject: [PATCH 05/12] Permit disabling of A* path cache at compile-time Mostly to measure its effectiveness. Compile with AS_NO_PATH_CACHE defined to disable it. Turns out the path cache is quite effective. For my continental test case (Hvy Metal 2 updates), it reduces the number of searches by a factor of 18.5, speeding up distribution path assembly by a factor of 7. The price is memory: it uses 135 times more memory than the A* library. For my island test case (Hvy Plastic 2 updates), I get 4 times search reduction, 3.5 times faster distribution path assembly, 36 times more memory. --- src/lib/common/path.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/lib/common/path.c b/src/lib/common/path.c index 9cdf959e..f36d4828 100644 --- a/src/lib/common/path.c +++ b/src/lib/common/path.c @@ -26,7 +26,6 @@ * --- * * path.c: Empire/A* Interface code. - * Define AS_STATS for A* statistics. * * Known contributors to this file: * Phil Lapsley, 1991 @@ -35,6 +34,14 @@ * Steve McClure, 1997 */ +/* + * Define AS_STATS for A* statistics on stderr. + * + * Define AS_NO_PATH_CACHE to disable the path cache. The path cache + * saves a lot of work, but uses lots of memory. It should be a + * significant net win, unless you run out of memory. + */ + #include #include @@ -103,7 +110,11 @@ best_path(struct sctstr *from, struct sctstr *to, char *path, int mob_type) if (!mybestpath) mybestpath = bp_init(); adp = mybestpath->adp; +#ifdef AS_NO_PATH_CACHE + ap = NULL; +#else ap = as_find_cachepath(from->sct_x, from->sct_y, to->sct_x, to->sct_y); +#endif if (ap == NULL) { adp->from.x = from->sct_x; adp->from.y = from->sct_y; @@ -278,19 +289,25 @@ bp_coord_hash(struct as_coord c) void bp_enable_cachepath(void) { +#ifndef AS_NO_PATH_CACHE as_enable_cachepath(); +#endif } void bp_disable_cachepath(void) { +#ifndef AS_NO_PATH_CACHE as_disable_cachepath(); +#endif } void bp_clear_cachepath(void) { +#ifndef AS_NO_PATH_CACHE as_clear_cachepath(); +#endif } double From 7edcd3ea771f1894f0cf06d0057328837b4cfd17 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sun, 20 Feb 2011 08:43:44 +0100 Subject: [PATCH 06/12] Permit disabling of A* neighbor cache at compile-time Mostly to measure its effectiveness. Compile with AS_NO_NEIGHBOR_CACHE defined to disable it. The neighbor cache turns out to be useless in my tests: it eats memory without speeding up the update's distribution path assembly. --- src/lib/common/path.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/common/path.c b/src/lib/common/path.c index f36d4828..58fded85 100644 --- a/src/lib/common/path.c +++ b/src/lib/common/path.c @@ -40,6 +40,10 @@ * Define AS_NO_PATH_CACHE to disable the path cache. The path cache * saves a lot of work, but uses lots of memory. It should be a * significant net win, unless you run out of memory. + * + * Define AS_NO_NEIGHBOR_CACHE to disable the neighbor cache. The + * neighbor cache trades a modest amount of memory to save a bit of + * work. In its current form, it doesn't really make a difference. */ #include @@ -87,8 +91,10 @@ bp_init(void) if (bp->adp == NULL) return NULL; +#ifndef AS_NO_NEIGHBOR_CACHE if (neighsects == NULL) neighsects = calloc(WORLD_SZ() * 6, sizeof(struct sctstr *)); +#endif return bp; } @@ -125,8 +131,10 @@ best_path(struct sctstr *from, struct sctstr *to, char *path, int mob_type) res = as_search(adp); #ifdef AS_STATS as_stats(adp, stderr); +#ifndef AS_NO_NEIGHBOR_CACHE fprintf(stderr, "neighbor cache %zu bytes\n", WORLD_SZ() * 6 * sizeof(struct sctstr *)); +#endif #endif if (res < 0) return -1; From e8825678899e4407214be76dc2117d745e3f70e2 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Thu, 24 Feb 2011 19:29:33 +0100 Subject: [PATCH 07/12] Speed up A* neighbor cache hits struct sctstr members sct_x, sct_y are normalized, no need to normalize them again. The neighbor cache now speeds up distribution path assembly by about 10% without the path cache, and by about 5% with the path cache. --- src/lib/common/path.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/common/path.c b/src/lib/common/path.c index 58fded85..7350e309 100644 --- a/src/lib/common/path.c +++ b/src/lib/common/path.c @@ -227,8 +227,8 @@ bp_neighbors(struct as_coord c, struct as_coord *cp, void *pp) *ssp = sp; } else { sp = *ssp; - sx = XNORM(sp->sct_x); - sy = YNORM(sp->sct_y); + sx = sp->sct_x; + sy = sp->sct_y; } /* No need to calculate cost each time, just make sure we can move through it. We calculate it later. */ From e9307b7b1d28bc8310a633fc21037dd694245b74 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Thu, 17 Mar 2011 04:26:45 +0100 Subject: [PATCH 08/12] Regression test-bed for new path finder Define TEST_PATH_FIND to run both the new and the old code, and verify they yield the same path costs. --- src/lib/update/finish.c | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/lib/update/finish.c b/src/lib/update/finish.c index 9da4a887..25b14d87 100644 --- a/src/lib/update/finish.c +++ b/src/lib/update/finish.c @@ -35,6 +35,8 @@ #include +#include +#include #include #include #include "distribute.h" @@ -122,7 +124,7 @@ finish_sects(int etu) } -#ifdef USE_PATH_FIND +#if defined(USE_PATH_FIND) || defined(TEST_PATH_FIND) static int distcmp(const void *p, const void *q) { @@ -147,7 +149,7 @@ assemble_dist_paths(double *import_cost) struct sctstr *sp; struct sctstr *dist; int n; -#ifdef USE_PATH_FIND +#if defined(USE_PATH_FIND) || defined(TEST_PATH_FIND) static int *job; int uid, i; coord dx = 1, dy = 0; /* invalid */ @@ -185,13 +187,16 @@ assemble_dist_paths(double *import_cost) } import_cost[uid] = path_find_to(sp->sct_x, sp->sct_y); } - path_find_print_stats(); -#else /* !USE_PATH_FIND */ +#endif /* USE_PATH_FIND || TEST_PATH_FIND */ +#if !defined(USE_PATH_FIND) || defined(TEST_PATH_FIND) char *path; double d; char buf[512]; for (n = 0; NULL != (sp = getsectid(n)); n++) { +#ifdef TEST_PATH_FIND + double new_imc = import_cost[n]; +#endif import_cost[n] = -1; if ((sp->sct_dist_x == sp->sct_x) && (sp->sct_dist_y == sp->sct_y)) continue; @@ -206,6 +211,25 @@ assemble_dist_paths(double *import_cost) path = BestDistPath(buf, dist, sp, &d); if (path) import_cost[n] = d; +#ifdef TEST_PATH_FIND + if (fabs(import_cost[n] - new_imc) >= 1e-6) { + printf("%d,%d <- %d,%d %d: old %g, new %g, %g off\n", + sp->sct_dist_x, sp->sct_dist_y, + sp->sct_x, sp->sct_y, MOB_MOVE, + import_cost[n], new_imc, import_cost[n] - new_imc); + printf("\told: %s\n", path); + d = path_find(sp->sct_dist_x, sp->sct_dist_y, + sp->sct_x, sp->sct_y, dist->sct_own, MOB_MOVE); + assert(d - new_imc < 1e-6); + path_find_route(buf, sizeof(buf), + sp->sct_dist_x, sp->sct_dist_y, + sp->sct_x, sp->sct_y); + printf("\tnew: %s\n", buf); + } +#endif /* TEST_PATH_FIND */ } -#endif /* !USE_PATH_FIND */ +#endif /* !USE_PATH_FIND || TEST_PATH_FIND */ +#if defined(USE_PATH_FIND) || defined(TEST_PATH_FIND) + path_find_print_stats(); +#endif } From 70ef6066f25ed3c86d83f8f7f907b1fa54fd4d26 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Mon, 21 Feb 2011 23:52:42 +0100 Subject: [PATCH 09/12] Compile-time option to switch off "multiple paths same source" Just to facilitate benchmarking. --- src/lib/update/finish.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/update/finish.c b/src/lib/update/finish.c index 25b14d87..8fe2cb2b 100644 --- a/src/lib/update/finish.c +++ b/src/lib/update/finish.c @@ -124,7 +124,7 @@ finish_sects(int etu) } -#if defined(USE_PATH_FIND) || defined(TEST_PATH_FIND) +#if (defined(USE_PATH_FIND) || defined(TEST_PATH_FIND)) && !defined(DIST_PATH_NO_REUSE) static int distcmp(const void *p, const void *q) { @@ -154,6 +154,12 @@ assemble_dist_paths(double *import_cost) int uid, i; coord dx = 1, dy = 0; /* invalid */ +#ifdef DIST_PATH_NO_REUSE + for (uid = 0; NULL != (sp = getsectid(uid)); uid++) { + import_cost[uid] = -1; + if (sp->sct_dist_x == sp->sct_x && sp->sct_dist_y == sp->sct_y) + continue; +#else if (!job) job = malloc(WORLD_SZ() * sizeof(*job)); @@ -174,18 +180,25 @@ assemble_dist_paths(double *import_cost) for (i = 0; i < n; i++) { uid = job[i]; +#endif /* !DIST_PATH_NO_REUSE */ sp = getsectid(uid); dist = getsectp(sp->sct_dist_x, sp->sct_dist_y); if (CANT_HAPPEN(!dist)) continue; if (sp->sct_own != dist->sct_own) continue; +#ifdef DIST_PATH_NO_REUSE + import_cost[uid] = path_find(sp->sct_dist_x, sp->sct_dist_y, + sp->sct_x, sp->sct_y, dist->sct_own, + MOB_MOVE); +#else if (sp->sct_dist_x != dx || sp->sct_dist_y != dy) { dx = sp->sct_dist_x; dy = sp->sct_dist_y; path_find_from(dx, dy, dist->sct_own, MOB_MOVE); } import_cost[uid] = path_find_to(sp->sct_x, sp->sct_y); +#endif } #endif /* USE_PATH_FIND || TEST_PATH_FIND */ #if !defined(USE_PATH_FIND) || defined(TEST_PATH_FIND) From b340b2d194d377fe522a61c90d154f88edd7c8c4 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sat, 26 Feb 2011 16:54:40 +0100 Subject: [PATCH 10/12] Cover sea & air in new path finder's regression test-bed Make TEST_PATH_FIND cover sea & air paths in addition to land paths. --- src/lib/common/path.c | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/lib/common/path.c b/src/lib/common/path.c index e901d485..ff907cbd 100644 --- a/src/lib/common/path.c +++ b/src/lib/common/path.c @@ -475,12 +475,64 @@ BestShipPath(char *path, int fx, int fy, int tx, int ty, int owner) map = ef_ptr(EF_BMAP, owner); if (!map) return NULL; +#ifdef TEST_PATH_FIND + double newc = path_find(fx, fy, tx, ty, owner, MOB_SAIL); + char *p = bestownedpath(path, map, fx, fy, tx, ty, owner); + double c; + size_t l; + char buf[MAX_PATH_LEN]; + + if (!p) + c = -1.0; + else { + l = strlen(p); + if (p[l-1] == 'h') + l--; + c = l; + } + + if (c != newc) { + path_find_route(buf, sizeof(buf), fx, fy, tx, ty); + printf("%d,%d -> %d,%d %d: old %g, new %g, %g off\n", + fx, fy, tx, ty, MOB_FLY, c, newc, c - newc); + printf("\told: %s\n", p); + printf("\tnew: %s\n", buf); + } + return p; +#else return bestownedpath(path, map, fx, fy, tx, ty, owner); +#endif } char * BestAirPath(char *path, int fx, int fy, int tx, int ty) { +#ifdef TEST_PATH_FIND + double newc = path_find(fx, fy, tx, ty, 0, MOB_FLY); + char *p = bestownedpath(path, NULL, fx, fy, tx, ty, -1); + double c; + size_t l; + char buf[MAX_PATH_LEN]; + + if (!p) + c = -1.0; + else { + l = strlen(p); + if (p[l-1] == 'h') + l--; + c = l; + } + + if (c != newc) { + path_find_route(buf, sizeof(buf), fx, fy, tx, ty); + printf("%d,%d -> %d,%d %d: old %g, new %g, %g off\n", + fx, fy, tx, ty, MOB_FLY, c, newc, c - newc); + printf("\told: %s\n", p); + printf("\tnew: %s\n", buf); + } + return p; +#else return bestownedpath(path, NULL, fx, fy, tx, ty, -1); +#endif } #endif /* !USE_PATH_FIND */ From ba08bfc0c92031920fe64878fc08b3d3b199006c Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sat, 5 Mar 2011 16:04:17 +0100 Subject: [PATCH 11/12] Generalize new path finder to A* A* is only usable for a single path, except with a heuristic function returning always zero, which turns it into Dijkstra's algorithm. Distribution uses it that way, because it needs to find multiple paths from the same source as efficiently as possible. Only the other uses of path search can profit from A*'s superior efficiency. I feel the extra complexity is not justified. Besides, it slows down distribution path assembly a bit, which is the only case where efficiency really matters. Let's stick to Dijkstra's for now. --- src/lib/common/pathfind.c | 134 ++++++++++++++++++++++++++------------ 1 file changed, 91 insertions(+), 43 deletions(-) diff --git a/src/lib/common/pathfind.c b/src/lib/common/pathfind.c index bd5cb121..283afb38 100644 --- a/src/lib/common/pathfind.c +++ b/src/lib/common/pathfind.c @@ -39,6 +39,7 @@ #include "file.h" #include "nat.h" #include "optlist.h" +#include "prototypes.h" #include "path.h" #include "sect.h" @@ -51,8 +52,8 @@ static char *bufrotate(char *buf, size_t bufsz, size_t i); /* - * Dijkstra's algorithm. Refer to your graph algorithm textbook for - * how it works. Implementation is specialized to hex maps. + * A* algorithm. Refer to your graph algorithm textbook for how it + * works. Implementation is specialized to hex maps. * * Define PATH_FIND_STATS for performance statistics on stdout. */ @@ -101,7 +102,7 @@ static struct pf_map *pf_map; struct pf_heap { int uid; /* sector uid and */ coord x, y; /* coordinates, uid == XYOFFSET(x, y) */ - double cost; /* cost from source */ + double f; /* estimated cost from source to dest */ }; static int pf_nheap; /* #entries in pf_nheap[] */ @@ -114,12 +115,13 @@ static coord pf_sx, pf_sy; static int pf_suid; static natid pf_actor; static double (*pf_sct_cost)(natid, int); +static double (*pf_h)(coord, coord, coord, coord, double); /* * Performance statistics */ #ifdef PATH_FIND_STATS -static unsigned pf_nsearch, pf_nsource, pf_nopen, pf_nclose; +static unsigned pf_nsearch, pf_nsource, pf_nopen, pf_nreopen, pf_nclose; static unsigned pf_nheap_max, pf_noway; static double pf_sumcost; #define STAT_INC(v) ((void)((v)++)) @@ -131,14 +133,12 @@ static double pf_sumcost; #define STAT_HIMARK(v, h) ((void)0) #endif /* !PATH_FIND_STATS */ -#ifndef NDEBUG /* silence "not used" warning */ /* Is sector with uid UID open? */ static int pf_is_open(int uid) { return pf_map[uid].visit == pf_visit; } -#endif /* Is sector with uid UID closed? */ static int @@ -169,11 +169,11 @@ pf_check(void) assert(0 <= uid && uid < WORLD_SZ()); assert(pf_map[uid].heapi == i); assert(pf_map[uid].visit == pf_visit); - assert(pf_map[uid].cost <= pf_heap[i].cost); + assert(pf_map[uid].cost <= pf_heap[i].f); c = 2 * i + 1; - assert(c >= pf_nheap || pf_heap[i].cost <= pf_heap[c].cost); + assert(c >= pf_nheap || pf_heap[i].f <= pf_heap[c].f); c++; - assert(c >= pf_nheap || pf_heap[i].cost <= pf_heap[c].cost); + assert(c >= pf_nheap || pf_heap[i].f <= pf_heap[c].f); } for (uid = 0; uid < WORLD_SZ(); uid++) { @@ -211,9 +211,9 @@ pf_sift_down(int n) assert(0 <= n && n < pf_nheap); for (r = n; (c = 2 * r + 1) < pf_nheap; r = c) { - if (c + 1 < pf_nheap && pf_heap[c].cost > pf_heap[c + 1].cost) + if (c + 1 < pf_nheap && pf_heap[c].f > pf_heap[c + 1].f) c++; - if (pf_heap[r].cost < pf_heap[c].cost) + if (pf_heap[r].f < pf_heap[c].f) break; pf_heap_swap(r, c); } @@ -227,7 +227,7 @@ pf_sift_up(int n) assert(0 <= n && n < pf_nheap); for (c = n; (p = (c - 1) / 2), c > 0; c = p) { - if (pf_heap[p].cost < pf_heap[c].cost) + if (pf_heap[p].f < pf_heap[c].f) break; pf_heap_swap(p, c); } @@ -237,25 +237,32 @@ pf_sift_up(int n) * Open the unvisited sector X,Y. * UID is sector uid, it equals XYOFFSET(X,Y). * Cheapest path from source comes from direction DIR and has cost COST. + * H is the estimated cost from X,Y to the destination. */ static void -pf_open(int uid, coord x, coord y, int dir, double cost) +pf_open(int uid, coord x, coord y, int dir, double cost, double h) { int i; - STAT_INC(pf_nopen); - i = pf_nheap++; - STAT_HIMARK(pf_nheap_max, (unsigned)pf_nheap); - DPRINTF("pf: open %d,%d %g %c %d\n", x, y, cost, dirch[dir], i); - assert(pf_is_unvisited(uid)); - pf_map[uid].visit = pf_visit; + if (pf_is_open(uid)) { + STAT_INC(pf_nreopen); + i = pf_map[uid].heapi; + DPRINTF("pf: reopen %d,%d %g %c %d\n", x, y, cost, dirch[dir], i); + } else { + assert(pf_is_unvisited(uid)); + STAT_INC(pf_nopen); + i = pf_nheap++; + STAT_HIMARK(pf_nheap_max, (unsigned)pf_nheap); + DPRINTF("pf: open %d,%d %g %c %d\n", x, y, cost, dirch[dir], i); + pf_map[uid].heapi = i; + pf_map[uid].visit = pf_visit; + pf_heap[i].uid = uid; + pf_heap[i].x = x; + pf_heap[i].y = y; + } pf_map[uid].dir = dir; - pf_map[uid].heapi = i; pf_map[uid].cost = cost; - pf_heap[i].uid = uid; - pf_heap[i].x = x; - pf_heap[i].y = y; - pf_heap[i].cost = cost; + pf_heap[i].f = cost + h; pf_sift_up(i); pf_check(); @@ -285,7 +292,7 @@ pf_close(void) #ifdef PATH_FIND_DEBUG /* * Return cost from source to sector with uid UID. - * It must be visited, i.e. open or closed. + * It must be open (cost is an estimate) or closed (cost is exact). */ static double pf_cost(int uid) @@ -337,9 +344,17 @@ rev_dir(int dir) * SX,SY is the source. * The cost to enter the sector with uid u is COST(ACTOR, u). * Negative value means the sector can't be entered. + * Optional H() is the heuristic: the cost from x,y to the destination + * dx,dy is at least H(x, y, dx, dy, c1d), where c1d is the cost to + * enter dx,dy. The closer H() is to the true cost, the more + * efficient this function works. + * With a null H(), A* degenerates into Dijkstra's algorithm. You can + * then call path_find_to() multiple times for the same source. This + * is faster when you need many destinations. */ static void -pf_set_source(coord sx, coord sy, natid actor, double (*cost)(natid, int)) +pf_set_source(coord sx, coord sy, natid actor, double (*cost) (natid, int), + double (*h) (coord, coord, coord, coord, double)) { STAT_INC(pf_nsource); DPRINTF("pf: source %d,%d\n", sx, sy); @@ -348,6 +363,7 @@ pf_set_source(coord sx, coord sy, natid actor, double (*cost)(natid, int)) pf_suid = XYOFFSET(sx, sy); pf_actor = actor; pf_sct_cost = cost; + pf_h = h; if (!pf_map) { pf_map = calloc(WORLD_SZ(), sizeof(*pf_map)); @@ -361,8 +377,6 @@ pf_set_source(coord sx, coord sy, natid actor, double (*cost)(natid, int)) pf_visit += 2; pf_nheap = 0; - - pf_open(pf_suid, pf_sx, pf_sy, DIR_STOP, 0.0); } /* @@ -373,7 +387,7 @@ path_find_to(coord dx, coord dy) { int duid; int uid, nuid, i; - double cost, c1; + double c1d, cost, c1; coord x, y, nx, ny; STAT_INC(pf_nsearch); @@ -385,29 +399,38 @@ path_find_to(coord dx, coord dy) return pf_map[duid].cost; } + c1d = pf_sct_cost(pf_actor, duid); + if (c1d < 0) { + DPRINTF("pf: done new %g\n", -1.0); + STAT_INC(pf_noway); + return -1; + } + + if (pf_is_unvisited(pf_suid)) + /* first search from this source */ + pf_open(pf_suid, pf_sx, pf_sy, DIR_STOP, 0.0, + pf_h ? pf_h(pf_sx, pf_sy, dx, dy, c1d) : 0.0); + else + assert(!pf_h); /* multiple searches only w/o heuristic */ + while (pf_nheap > 0 && (uid = pf_heap[0].uid) != duid) { x = pf_heap[0].x; y = pf_heap[0].y; - cost = pf_heap[0].cost; pf_close(); + cost = pf_map[uid].cost; for (i = 0; i < 6; i++) { /* for all neighbors */ nx = x_in_dir(x, DIR_FIRST + i); ny = y_in_dir(y, DIR_FIRST + i); nuid = XYOFFSET(nx, ny); - /* - * Cost to enter NX,NY doesn't depend on direction of - * entry. This X,Y is at least as expensive as any - * previous one. Therefore, cost to go to NX,NY via X,Y - * is at least as high as any previously found route. - * Skip neighbors that have a route already. - */ - if (!pf_is_unvisited(nuid)) + if (pf_h ? pf_is_closed(nuid) : !pf_is_unvisited(nuid)) continue; c1 = pf_sct_cost(pf_actor, nuid); if (c1 < 0) continue; - pf_open(nuid, nx, ny, DIR_FIRST + i, cost + c1); + if (pf_is_unvisited(nuid) || cost + c1 < pf_map[nuid].cost) + pf_open(nuid, nx, ny, DIR_FIRST + i, cost + c1, + pf_h ? pf_h(nx, ny, dx, dy, c1d) : 0); } } @@ -555,9 +578,10 @@ path_find_visualize(coord sx, coord sy, coord dx, coord dy) void path_find_print_stats(void) { - printf("pathfind %u searches, %u sources, %u opened, %u closed," + printf("pathfind %u searches, %u sources," + " %u opened, %u reopened, %u closed," " %u heap max, %zu bytes, %u noway, %g avg cost\n", - pf_nsearch, pf_nsource, pf_nopen, pf_nclose, + pf_nsearch, pf_nsource, pf_nopen, pf_nreopen, pf_nclose, pf_nheap_max, (WORLD_SZ() * (sizeof(*pf_map) + sizeof(*pf_heap))), pf_noway, pf_nsearch ? pf_sumcost / pf_nsearch : 0.0); @@ -599,6 +623,20 @@ cost_rail(natid actor, int uid) return cost_land(actor, uid, MOB_RAIL); } +static double +h_move(coord x, coord y, coord dx, coord dy, double c1d) +{ + int md = mapdist(x, y, dx, dy); + return md ? c1d + (md - 1) * 0.001 : 0.0; +} + +static double +h_march_rail(coord x, coord y, coord dx, coord dy, double c1d) +{ + int md = mapdist(x, y, dx, dy); + return md ? c1d + (md - 1) * 0.02 : 0.0; +} + static double cost_sail(natid actor, int uid) { @@ -634,10 +672,20 @@ cost_fly(natid actor, int uid) return 1.0; } +static double +h_sail_fly(coord x, coord y, coord dx, coord dy, double c1d) +{ + return mapdist(x, y, dx, dy); +} + static double (*cost_tab[])(natid, int) = { cost_move, cost_march, cost_rail, cost_sail, cost_fly }; +static double (*h[])(coord, coord, coord, coord, double) = { + h_move, h_march_rail, h_march_rail, h_sail_fly, h_sail_fly +}; + /* * Start finding paths from SX,SY. * Use mobility costs for ACTOR and MOBTYPE. @@ -645,7 +693,7 @@ static double (*cost_tab[])(natid, int) = { void path_find_from(coord sx, coord sy, natid actor, int mobtype) { - pf_set_source(sx, sy, actor, cost_tab[mobtype]); + pf_set_source(sx, sy, actor, cost_tab[mobtype], NULL); } /* @@ -655,6 +703,6 @@ path_find_from(coord sx, coord sy, natid actor, int mobtype) double path_find(coord sx, coord sy, coord dx, coord dy, natid actor, int mobtype) { - pf_set_source(sx, sy, actor, cost_tab[mobtype]); + pf_set_source(sx, sy, actor, cost_tab[mobtype], h[mobtype]); return path_find_to(dx, dy); } From b5b3a7b1efca6ba2ccb29d5a67cc1d8a75074933 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Sat, 5 Mar 2011 16:05:26 +0100 Subject: [PATCH 12/12] Compile-time option to use A* for distribution Just for benchmarking. --- src/lib/update/finish.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/update/finish.c b/src/lib/update/finish.c index 8fe2cb2b..16c9872d 100644 --- a/src/lib/update/finish.c +++ b/src/lib/update/finish.c @@ -188,9 +188,15 @@ assemble_dist_paths(double *import_cost) if (sp->sct_own != dist->sct_own) continue; #ifdef DIST_PATH_NO_REUSE +#if DIST_PATH_NO_REUSE == 1 import_cost[uid] = path_find(sp->sct_dist_x, sp->sct_dist_y, sp->sct_x, sp->sct_y, dist->sct_own, MOB_MOVE); +#else + path_find_from(sp->sct_dist_x, sp->sct_dist_y, + dist->sct_own, MOB_MOVE); + import_cost[uid] = path_find_to(sp->sct_x, sp->sct_y); +#endif #else if (sp->sct_dist_x != dx || sp->sct_dist_y != dy) { dx = sp->sct_dist_x;