OpenCog Framework  Branch: master, revision 6f0b7fc776b08468cf1b74aa9db028f387b4f0c0
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
benchmark.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 
4 """
5 Python binding benchmarks
6 
7 Displays the number of operations per second achieved under various scenarios
8 utilizing the Python bindings that wrap the AtomSpace using Cython. Some of the
9 tests compare the evaluation of nodes using the Python Scheme bindings which
10 take much longer than their comparable Cython bindings.
11 
12 Results can be compared with the more comprehensive benchmarking code in
13 the opencog/benchmark directory.
14 
15 Example results (excerpt only):
16 --- OpenCog Python Benchmark - 2015-03-12 15:44:02.614126 ---
17 
18 -- Testing Node Adds --
19 
20 Add nodes - Cython
21  Total time: 0.537
22  Ops: 100,000
23  Op time in µs: 5.37
24  Ops per second: 186,219
25 
26 Add nodes - Cython (500K)
27  Total time: 2.792
28  Ops: 500,000
29  Op time in µs: 5.58
30  Ops per second: 179,115
31 
32 Add fully connected nodes
33  Total time: 5.049
34  Ops: 500,500
35  Op time in µs: 10.09
36  Ops per second: 99,132
37 
38 ...
39 
40 """
41 
42 import argparse
43 import time, datetime
44 from opencog.atomspace import AtomSpace, TruthValue, Handle, Atom, types
45 import opencog.scheme_wrapper as scheme
46 from opencog.scheme_wrapper import load_scm, scheme_eval, scheme_eval_h
47 from opencog.bindlink import stub_bindlink, bindlink
48 
49 __authors__ = 'Cosmo Harrigan, Curtis Faith'
50 
51 class SmartFormatter(argparse.HelpFormatter):
52 
53  def _split_lines(self, text, width):
54  # this is the RawTextHelpFormatter._split_lines
55  if text.startswith('R|'):
56  return text[2:].splitlines()
57  return argparse.HelpFormatter._split_lines(self, text, width)
58 
59 # Setup the argument parser.
60 parser = argparse.ArgumentParser(description="OpenCog Python Benchmark",
61  formatter_class=SmartFormatter)
62 verbosity_group = parser.add_mutually_exclusive_group()
63 verbosity_group.add_argument("-v", "--verbose", action="store_true", help="verbose output")
64 verbosity_group.add_argument("-c", "--columns", action="store_true", default=True, help="columnar output (default)")
65 #verbosity_group.add_argument("-q", "--quiet", action="store_true", help="minimal output")
66 test_group = parser.add_mutually_exclusive_group()
67 test_group.add_argument("-a", "--all", default=False, action="store_true", help="run all tests")
68 test_group.add_argument("-t", "--test", type=str, default='spread',
69  choices=['spread','node', 'bindlink', 'traverse', 'scheme', 'get_vs_xget', 'predicates'],
70  help="R|Test to benchmark, where:\n"
71  " spread - a spread of tests across areas (default)\n"
72  " node - atomspace node operations \n"
73  " bindlink - all the bindlink forms\n"
74  " traverse - traversal of atomspace lists\n"
75  " scheme - scheme evaluation functions\n"
76  " get_vs_xget - compare get to xget functions\n"
77  " predicates - predicate retrieval functions")
78 parser.add_argument("-i", "--iterations", metavar='N', type=int, default=10, help="iterations to average (default=10)")
79 args = parser.parse_args()
80 
81 if args.verbose:
82  args.columns = False
83 
84 # Number of test iterations to average for timing.
85 test_iterations = args.iterations
86 
87 scheme_preload = [
88  "opencog/atomspace/core_types.scm",
89  "opencog/scm/utilities.scm"
90  ]
91 
93  start = time.time()
94  n = 1000000
95  for i in xrange(n):
96  pass
97  finish = time.time()
98  return ((finish - start) / n)
99 
100 
101 # Preps - Perform any untimed one-time setup required for a test.
102 
103 def prep_none(atomspace):
104  pass
105 
106 def prep_traverse_10K(atomspace):
107  """Add n nodes in atomspace with python bindings"""
108  n = 10000
109  for i in xrange(n):
110  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
111  return n, atomspace.get_atoms_by_type(types.ConceptNode)
112 
113 def prep_traverse_100K(atomspace):
114  """Add n nodes in atomspace with python bindings"""
115  n = 100000
116  for i in xrange(n):
117  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
118  return n, atomspace.get_atoms_by_type(types.ConceptNode)
119 
120 def prep_traverse_1M(atomspace):
121  """Add n nodes in atomspace with python bindings"""
122  n = 1000000
123  for i in xrange(n):
124  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
125  return n, atomspace.get_atoms_by_type(types.ConceptNode)
126 
127 def prep_get_100K(atomspace):
128  """Add n nodes in atomspace with python bindings"""
129  n = 100000
130  for i in xrange(n):
131  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
132  return n
133 
134 def prep_get_outgoing(atomspace):
135  """Add n nodes and create a complete (fully-connected) graph in atomspace
136  and returns the number of items processed
137  """
138  n = 1000
139  offset = atomspace.add_node(types.ConceptNode, "Starting handle offset")
140  offset = offset.h.value()
141 
142  for i in xrange(1, n+1):
143  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
144  for j in xrange(1, i):
145  atomspace.add_link(types.HebbianLink,
146  [Handle(i + offset), Handle(j + offset)],
147  TruthValue(.2, .3))
148 
149  # Number of vertices plus number of edges in a fully connected graph
150  return n + (n**2 - n) / 2
151 
152 def prep_scheme(atomspace):
153  scheme.__init__(atomspace)
154 
155 def prep_bind(atomspace):
156  scheme.__init__(atomspace)
157  for scheme_file in scheme_preload:
158  load_scm(atomspace, scheme_file)
159 
160  # Define several animals and something of a different type as well
161  scheme_animals = \
162  '''
163  (InheritanceLink (ConceptNode "Frog") (ConceptNode "animal"))
164  (InheritanceLink (ConceptNode "Zebra") (ConceptNode "animal"))
165  (InheritanceLink (ConceptNode "Deer") (ConceptNode "animal"))
166  (InheritanceLink (ConceptNode "Spaceship") (ConceptNode "machine"))
167  '''
168  scheme_eval_h(atomspace, scheme_animals)
169 
170 def prep_bind_python(atomspace):
171  prep_bind(atomspace)
172 
173  # Define a graph search query
174  bind_link_query = \
175  '''
176  (BindLink
177  ;; The variable to be bound
178  (VariableNode "$var")
179  ;; The pattern to be searched for
180  (InheritanceLink
181  (VariableNode "$var")
182  (ConceptNode "animal")
183  )
184  ;; The value to be returned.
185  (VariableNode "$var")
186  )
187  '''
188  return scheme_eval_h(atomspace, bind_link_query)
189 
190 def prep_bind_scheme(atomspace):
191  prep_bind(atomspace)
192 
193  # Define a graph search query
194  scheme_query = \
195  '''
196  (define find-animals
197  (BindLink
198  ;; The variable to be bound
199  (VariableNode "$var")
200  ;; The pattern to be searched for
201  (InheritanceLink
202  (VariableNode "$var")
203  (ConceptNode "animal")
204  )
205 
206  ;; The value to be returned.
207  (VariableNode "$var")
208  )
209  )
210  '''
211  scheme_eval_h(atomspace, scheme_query)
212 
213 def prep_predicates(atomspace):
214  scheme.__init__(atomspace)
215  for scheme_file in scheme_preload:
216  load_scm(atomspace, scheme_file)
217 
218  # Define dogs relationships
219  scheme_dog_predicates = \
220  '''
221  (EvaluationLink
222  (PredicateNode "IsA")
223  (ListLink
224  (ConceptNode "dog")
225  (ConceptNode "mammal")
226  )
227  )
228  (EvaluationLink
229  (PredicateNode "IsA")
230  (ListLink
231  (ConceptNode "dog")
232  (ConceptNode "animal")
233  )
234  )
235  (EvaluationLink
236  (PredicateNode "Loves")
237  (ListLink
238  (ConceptNode "dog")
239  (ConceptNode "biscuits")
240  )
241  )
242  '''
243  scheme_eval_h(atomspace, scheme_dog_predicates)
244  dog = atomspace.add_node(types.ConceptNode, "dog")
245  isA = atomspace.add_node(types.PredicateNode, "IsA")
246  return dog, isA
247 
248 # Tests
249 
250 def test_add_nodes(atomspace, prep_handle):
251  """Add n nodes in atomspace with python bindings"""
252  n = 100000
253  for i in xrange(n):
254  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
255  return n
256 
257 def test_add_nodes_large(atomspace, prep_handle):
258  """Add n nodes in atomspace with python bindings"""
259  n = 500000
260  for i in xrange(n):
261  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
262  return n
263 
264 def test_add_connected(atomspace, prep_handle):
265  """Add n nodes and create a complete (fully-connected) graph in atomspace
266  and returns the number of items processed
267  """
268  n = 1000
269  offset = atomspace.add_node(types.ConceptNode, "Starting handle offset")
270  offset = offset.h.value()
271 
272  for i in xrange(1, n+1):
273  atomspace.add_node(types.ConceptNode, str(i), TruthValue(.5, .5))
274  for j in xrange(1, i):
275  atomspace.add_link(types.HebbianLink,
276  [Handle(i + offset), Handle(j + offset)],
277  TruthValue(.2, .3))
278 
279  # Number of vertices plus number of edges in a fully connected graph
280  return n + (n**2 - n) / 2
281 
282 
283 # Traversal tests
284 def test_bare_traversal(atomspace, prep_traverse_result):
285  atom_count, atom_list = prep_traverse_result
286  for atom in atom_list:
287  pass
288 
289  # Prep for traversal returned the node count. The above iterates all.
290  return atom_count
291 
292 def test_resolve_traversal(atomspace, prep_traverse_result):
293  atom_count, atom_list = prep_traverse_result
294  for atom in atom_list:
295  if not atom:
296  print "test_resolve_traversal - atom didn't resolve"
297  break
298 
299  # Prep for traversal returned the node count. The above iterates all.
300  return atom_count
301 
302 def test_get_traverse(atomspace, prep_result):
303  atom_count = prep_result
304  atom_list = atomspace.get_atoms_by_type(types.ConceptNode)
305  for atom in atom_list:
306  pass
307 
308  # Prep returned the atom count. The above iterates all.
309  return atom_count
310 
311 def test_xget_traverse(atomspace, prep_result):
312  atom_count = prep_result
313  for atom in atomspace.xget_atoms_by_type(types.ConceptNode):
314  pass
315 
316  # Prep returned the atom count. The above iterates all.
317  return atom_count
318 
319 def test_get_outgoing(atomspace, prep_result):
320  atom_count = prep_result
321  atom_list = atomspace.get_atoms_by_type(types.HebbianLink)
322  count = 0
323  for atom in atom_list:
324  outgoing_list = atomspace.get_outgoing(atom.h)
325  for outgoing in outgoing_list:
326  count += 1
327  return count
328 
329 def test_get_outgoing_no_list(atomspace, prep_result):
330  atom_count = prep_result
331  count = 0
332  for atom in atomspace.get_atoms_by_type(types.HebbianLink):
333  for outgoing in atomspace.get_outgoing(atom.h):
334  count += 1
335  return count
336 
337 def test_xget_outgoing(atomspace, prep_result):
338  atom_count = prep_result
339  count = 0
340  for atom in atomspace.xget_atoms_by_type(types.HebbianLink):
341  for outgoing in atomspace.xget_outgoing(atom.h):
342  count += 1
343  return count
344 
345 
346 # Bindlink tests
347 
348 def test_stub_bindlink(atomspace, prep_handle):
349  n = 100000
350  for i in xrange(n):
351  result = stub_bindlink(atomspace, prep_handle)
352  return n
353 
354 def test_bind(atomspace, prep_handle):
355  n = 10000
356  for i in xrange(n):
357  result = bindlink(atomspace, prep_handle)
358  return n
359 
360 
361 # Scheme tests
362 
363 def test_scheme_eval(atomspace, prep_handle):
364  n = 10000
365  for i in xrange(n):
366  result = scheme_eval_h(atomspace, '(+ 2 2)')
367  return n
368 
369 def test_bind_scheme(atomspace, prep_handle):
370  n = 10000
371  for i in xrange(n):
372  result = scheme_eval_h(atomspace, '(cog-bind find-animals)')
373  return n
374 
375 def test_add_nodes_scheme(atomspace, prep_handle):
376  """Add n nodes in atomspace using scheme"""
377  n = 10000
378  for i in xrange(n):
379  scheme = 'cog-new-node \'ConceptNode "' + str(i) + \
380  '" (cog-new-stv 0.5 0.5)'
381  scheme_eval_h(atomspace, scheme)
382  return n
383 
384 def test_add_nodes_sugar(atomspace, prep_handle):
385  """Add n nodes in atomspace using scheme with syntactic sugar"""
386  n = 10000
387  for i in xrange(n):
388  scheme = '\'ConceptNode "' + str(i) + '" (cog-new-stv 0.5 0.5)'
389  scheme_eval_h(atomspace, scheme)
390  return n
391 
392 # Predicate tests
393 def test_get_predicates(atomspace, prep_result):
394  dog, isA = prep_result
395  n = 100000
396  for i in xrange(n):
397  result = atomspace.get_predicates(dog)
398  return n
399 
400 def test_get_predicates_for(atomspace, prep_result):
401  dog, isA = prep_result
402  n = 100000
403  for i in xrange(n):
404  result = atomspace.get_predicates_for(dog, isA)
405  return n
406 
407 def test_get_predicates_scheme(atomspace, prep_result):
408  scheme = '(cog-get-pred (ConceptNode "dog") \'PredicateNode)'
409  n = 1000
410  for i in xrange(n):
411  scheme_eval_h(atomspace, scheme)
412  return n
413 
414 # Run the prep, then the test.
415 # Note: the AtomSpace gets deleted when this function completes and
416 # atomspace goes out of scope. So each test begins with a new AtomSpace.
417 def do_test(prep, test, description, op_time_adjustment):
418  """Runs tests and prints the test name and performance"""
419  if not args.columns:
420  print description
421  if (test != None):
422  total_time = 0
423  total_ops = 0
424  for i in xrange(test_iterations):
425  # Prep the test
426  atomspace = AtomSpace()
427  prep_result = prep(atomspace)
428 
429  # Time the test
430  start = time.time()
431  ops = test(atomspace, prep_result)
432  finish = time.time()
433 
434  # Add the time and ops for this iteration
435  total_time += finish - start
436  total_ops += ops
437 
438  average_time = total_time / test_iterations
439  average_ops = total_ops / test_iterations
440  adjusted_time = average_time - (ops * op_time_adjustment)
441 
442  # Print timing results
443  if args.verbose:
444  time_string = "{0:.03f}".format(adjusted_time)
445  print " Total: {0: >12}s".format(time_string)
446 
447  test_ops = "{0:,d}".format(int(average_ops))
448  print " Ops: {0: >12}".format(test_ops)
449 
450  op_time = "{0:.03f}".format(adjusted_time * 1000000 / ops)
451  ops_sec = "{0:,d}".format(int(ops / adjusted_time))
452  if args.columns:
453  print "{0: <40} {1: >12}µs {2: >15}".format(description, op_time,
454  ops_sec)
455  else:
456  print " Op time: {0: >12}µs".format(op_time)
457  print " Ops/sec: {0: >12}".format(ops_sec)
458  print
459 
460 # The preps column contains functions that setup each test that are not timed.
461 # The result returned by the prep is passed through to the test so you can
462 # build complex predicates etc. without affecting test times.
463 # Prep function # Test function # Test description
464 tests = [
465 (['all'], None, None, "-- Testing Node Adds --"),
466 (['node','spread'], prep_none, test_add_nodes, "Add nodes - Cython"),
467 (['node'], prep_none, test_add_nodes_large, "Add nodes - Cython (500K)"),
468 (['node'], prep_none, test_add_connected, "Add fully connected nodes"),
469 
470 (['all'], None, None, "-- Testing Atom Traversal --"),
471 (['traverse'], prep_traverse_100K, test_bare_traversal, "Bare atom traversal 100K - by type"),
472 (['traverse'], prep_traverse_10K, test_resolve_traversal, "Resolve Handle 10K - by type"),
473 (['traverse','spread'], prep_traverse_100K, test_resolve_traversal, "Resolve Handle 100K - by type"),
474 (['traverse'], prep_traverse_1M, test_resolve_traversal, "Resolve Handle 1M - by type"),
475 
476 (['all'], None, None, "-- Testing Get versus Yield-based Get --"),
477 (['get_vs_xget'], prep_get_100K, test_get_traverse, "Get and Traverse 100K - by type"),
478 (['get_vs_xget'], prep_get_100K, test_xget_traverse, "Yield Get and Traverse 100K - by type"),
479 (['get_vs_xget'], prep_get_outgoing, test_get_outgoing, "Get Outgoing"),
480 (['get_vs_xget'], prep_get_outgoing, test_get_outgoing_no_list, "Get Outgoing - no temporary list"),
481 (['get_vs_xget'], prep_get_outgoing, test_xget_outgoing, "Yield Get Outgoing"),
482 
483 (['all'], None, None, "-- Testing Bind --"),
484 (['bindlink'], prep_none, test_stub_bindlink, "Bind - stub_bindlink - Cython"),
485 (['bindlink','spread'], prep_bind_python, test_bind, "Bind - bindlink - Cython"),
486 
487 (['all'], None, None, "-- Testing Scheme Eval --"),
488 (['scheme','spread'], prep_scheme, test_scheme_eval, "Test scheme_eval_h(+ 2 2)"),
489 (['scheme'], prep_bind_scheme, test_bind_scheme, "Bind - cog-bind - Scheme"),
490 (['scheme'], prep_scheme, test_add_nodes_scheme, "Add nodes - cog-new-node - Scheme"),
491 (['scheme'], prep_scheme, test_add_nodes_sugar, "Add nodes - ConceptNode sugar - Scheme"),
492 
493 (['all'], None, None, "-- Testing Get Predicates --"),
494 (['predicates','spread'], prep_predicates, test_get_predicates, "Predicates - get_predicates"),
495 (['predicates'], prep_predicates, test_get_predicates_for, "Predicates - get_predicates_for"),
496 (['predicates'], prep_predicates, test_get_predicates_scheme, "Predicates - cog-get-pred - Scheme"),
497 ]
498 
499 
500 print
501 print "--- OpenCog Python Benchmark - ", str(datetime.datetime.now()), "---"
502 print
503 
504 # Get the per-op adjustment to account for the time of the loops themselves.
505 op_time_adjustment = loop_time_per_op()
506 if args.verbose or args.iterations != 10:
507  if args.iterations > 1:
508  print "Test times averaged over {0:d} iterations.".format(test_iterations)
509  else:
510  print "Test times averaged over 1 iteration."
511 if not args.verbose and args.iterations != 10:
512  print
513 
514 if args.verbose:
515  print "Per-op loop adjustment: {0:.03f}µs.".format(op_time_adjustment * 1000000)
516  print
517 
518 if args.columns:
519  print "{0: <40} {1: >14} {2: >15}".format("Test", "Time per op", "Ops per second")
520  print "{0: <40} {1: >14} {2: >15}".format("----", "-----------", "--------------")
521 
522 # Run all the tests.
523 for run_list, prep, test, description in tests:
524  if args.all or args.test in run_list:
525  do_test(prep, test, description, op_time_adjustment)
526 
527 # Print an extra line to offset the benchmarks
528 print
def prep_traverse_100K
Definition: benchmark.py:113
def test_scheme_eval
Definition: benchmark.py:363
def test_get_outgoing_no_list
Definition: benchmark.py:329
def test_get_predicates
Definition: benchmark.py:393
def prep_predicates
Definition: benchmark.py:213
def test_add_nodes_scheme
Definition: benchmark.py:375
def test_add_nodes_large
Definition: benchmark.py:257
def do_test
Definition: benchmark.py:417
def test_get_predicates_for
Definition: benchmark.py:400
def prep_none
Definition: benchmark.py:103
def prep_scheme
Definition: benchmark.py:152
def prep_get_outgoing
Definition: benchmark.py:134
def test_stub_bindlink
Definition: benchmark.py:348
def test_add_nodes
Definition: benchmark.py:250
def prep_traverse_1M
Definition: benchmark.py:120
def prep_bind_python
Definition: benchmark.py:170
def test_bind_scheme
Definition: benchmark.py:369
def test_xget_outgoing
Definition: benchmark.py:337
def test_get_predicates_scheme
Definition: benchmark.py:407
def prep_get_100K
Definition: benchmark.py:127
def loop_time_per_op
Definition: benchmark.py:92
def test_get_outgoing
Definition: benchmark.py:319
Handle bindlink(AtomSpace *, const Handle &)
Definition: Implicator.cc:156
def prep_traverse_10K
Definition: benchmark.py:106
def test_get_traverse
Definition: benchmark.py:302
def test_resolve_traversal
Definition: benchmark.py:292
def test_bind
Definition: benchmark.py:354
def test_add_nodes_sugar
Definition: benchmark.py:384
def test_bare_traversal
Definition: benchmark.py:284
def test_xget_traverse
Definition: benchmark.py:311
def prep_bind_scheme
Definition: benchmark.py:190
Handle stub_bindlink(AtomSpace *, Handle)
Definition: BindlinkStub.cc:11
def test_add_connected
Definition: benchmark.py:264
def prep_bind
Definition: benchmark.py:155