blob: b2f01c2387ae2f8ecd8875e25bf13ae2746ac21a [file] [log] [blame]
Jim Cownie4cc4bb42014-10-07 16:25:50 +00001/** @file kmp_stats.cpp
2 * Statistics gathering and processing.
3 */
4
5
6//===----------------------------------------------------------------------===//
7//
8// The LLVM Compiler Infrastructure
9//
10// This file is dual licensed under the MIT and the University of Illinois Open
11// Source Licenses. See LICENSE.txt for details.
12//
13//===----------------------------------------------------------------------===//
14
Jim Cownie4cc4bb42014-10-07 16:25:50 +000015#include "kmp.h"
16#include "kmp_str.h"
17#include "kmp_lock.h"
18#include "kmp_stats.h"
19
20#include <algorithm>
21#include <sstream>
22#include <iomanip>
23#include <stdlib.h> // for atexit
24
25#define STRINGIZE2(x) #x
26#define STRINGIZE(x) STRINGIZE2(x)
27
28#define expandName(name,flags,ignore) {STRINGIZE(name),flags},
29statInfo timeStat::timerInfo[] = {
30 KMP_FOREACH_TIMER(expandName,0)
31 {0,0}
32};
33const statInfo counter::counterInfo[] = {
34 KMP_FOREACH_COUNTER(expandName,0)
35 {0,0}
36};
37#undef expandName
38
39#define expandName(ignore1,ignore2,ignore3) {0.0,0.0,0.0},
40kmp_stats_output_module::rgb_color kmp_stats_output_module::timerColorInfo[] = {
41 KMP_FOREACH_TIMER(expandName,0)
42 {0.0,0.0,0.0}
43};
44#undef expandName
45
46const kmp_stats_output_module::rgb_color kmp_stats_output_module::globalColorArray[] = {
47 {1.0, 0.0, 0.0}, // red
48 {1.0, 0.6, 0.0}, // orange
49 {1.0, 1.0, 0.0}, // yellow
50 {0.0, 1.0, 0.0}, // green
51 {0.0, 0.0, 1.0}, // blue
52 {0.6, 0.2, 0.8}, // purple
53 {1.0, 0.0, 1.0}, // magenta
54 {0.0, 0.4, 0.2}, // dark green
55 {1.0, 1.0, 0.6}, // light yellow
56 {0.6, 0.4, 0.6}, // dirty purple
57 {0.0, 1.0, 1.0}, // cyan
58 {1.0, 0.4, 0.8}, // pink
59 {0.5, 0.5, 0.5}, // grey
60 {0.8, 0.7, 0.5}, // brown
61 {0.6, 0.6, 1.0}, // light blue
62 {1.0, 0.7, 0.5}, // peach
63 {0.8, 0.5, 1.0}, // lavender
64 {0.6, 0.0, 0.0}, // dark red
65 {0.7, 0.6, 0.0}, // gold
66 {0.0, 0.0, 0.0} // black
67};
68
69// Ensure that the atexit handler only runs once.
70static uint32_t statsPrinted = 0;
71
72// output interface
73static kmp_stats_output_module __kmp_stats_global_output;
74
75/* ****************************************************** */
76/* ************* statistic member functions ************* */
77
78void statistic::addSample(double sample)
79{
80 double delta = sample - meanVal;
81
82 sampleCount = sampleCount + 1;
83 meanVal = meanVal + delta/sampleCount;
84 m2 = m2 + delta*(sample - meanVal);
85
86 minVal = std::min(minVal, sample);
87 maxVal = std::max(maxVal, sample);
88}
89
90statistic & statistic::operator+= (const statistic & other)
91{
92 if (sampleCount == 0)
93 {
94 *this = other;
95 return *this;
96 }
97
98 uint64_t newSampleCount = sampleCount + other.sampleCount;
99 double dnsc = double(newSampleCount);
100 double dsc = double(sampleCount);
101 double dscBydnsc = dsc/dnsc;
102 double dosc = double(other.sampleCount);
103 double delta = other.meanVal - meanVal;
104
105 // Try to order these calculations to avoid overflows.
106 // If this were Fortran, then the compiler would not be able to re-order over brackets.
107 // In C++ it may be legal to do that (we certainly hope it doesn't, and CC+ Programming Language 2nd edition
108 // suggests it shouldn't, since it says that exploitation of associativity can only be made if the operation
109 // really is associative (which floating addition isn't...)).
110 meanVal = meanVal*dscBydnsc + other.meanVal*(1-dscBydnsc);
111 m2 = m2 + other.m2 + dscBydnsc*dosc*delta*delta;
112 minVal = std::min (minVal, other.minVal);
113 maxVal = std::max (maxVal, other.maxVal);
114 sampleCount = newSampleCount;
115
116
117 return *this;
118}
119
120void statistic::scale(double factor)
121{
122 minVal = minVal*factor;
123 maxVal = maxVal*factor;
124 meanVal= meanVal*factor;
125 m2 = m2*factor*factor;
126 return;
127}
128
129std::string statistic::format(char unit, bool total) const
130{
131 std::string result = formatSI(sampleCount,9,' ');
Jonathan Peytonc1a7c972016-03-03 21:24:13 +0000132
133 if (sampleCount == 0)
134 {
135 result = result + std::string(", ") + formatSI(0.0, 9, unit);
136 result = result + std::string(", ") + formatSI(0.0, 9, unit);
137 result = result + std::string(", ") + formatSI(0.0, 9, unit);
138 if (total)
139 result = result + std::string(", ") + formatSI(0.0, 9, unit);
140 result = result + std::string(", ") + formatSI(0.0, 9, unit);
141 }
142 else
143 {
144 result = result + std::string(", ") + formatSI(minVal, 9, unit);
145 result = result + std::string(", ") + formatSI(meanVal, 9, unit);
146 result = result + std::string(", ") + formatSI(maxVal, 9, unit);
147 if (total)
148 result = result + std::string(", ") + formatSI(meanVal*sampleCount, 9, unit);
149 result = result + std::string(", ") + formatSI(getSD(), 9, unit);
150 }
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000151 return result;
152}
153
154/* ********************************************************** */
155/* ************* explicitTimer member functions ************* */
156
157void explicitTimer::start(timer_e timerEnumValue) {
158 startTime = tsc_tick_count::now();
159 if(timeStat::logEvent(timerEnumValue)) {
160 __kmp_stats_thread_ptr->incrementNestValue();
161 }
162 return;
163}
164
165void explicitTimer::stop(timer_e timerEnumValue) {
166 if (startTime.getValue() == 0)
167 return;
168
169 tsc_tick_count finishTime = tsc_tick_count::now();
170
171 //stat->addSample ((tsc_tick_count::now() - startTime).ticks());
172 stat->addSample ((finishTime - startTime).ticks());
173
174 if(timeStat::logEvent(timerEnumValue)) {
175 __kmp_stats_thread_ptr->push_event(startTime.getValue() - __kmp_stats_start_time.getValue(), finishTime.getValue() - __kmp_stats_start_time.getValue(), __kmp_stats_thread_ptr->getNestValue(), timerEnumValue);
176 __kmp_stats_thread_ptr->decrementNestValue();
177 }
178
179 /* We accept the risk that we drop a sample because it really did start at t==0. */
180 startTime = 0;
181 return;
182}
183
184/* ******************************************************************* */
185/* ************* kmp_stats_event_vector member functions ************* */
186
187void kmp_stats_event_vector::deallocate() {
188 __kmp_free(events);
189 internal_size = 0;
190 allocated_size = 0;
191 events = NULL;
192}
193
194// This function is for qsort() which requires the compare function to return
195// either a negative number if event1 < event2, a positive number if event1 > event2
196// or zero if event1 == event2.
197// This sorts by start time (lowest to highest).
198int compare_two_events(const void* event1, const void* event2) {
199 kmp_stats_event* ev1 = (kmp_stats_event*)event1;
200 kmp_stats_event* ev2 = (kmp_stats_event*)event2;
201
202 if(ev1->getStart() < ev2->getStart()) return -1;
203 else if(ev1->getStart() > ev2->getStart()) return 1;
204 else return 0;
205}
206
207void kmp_stats_event_vector::sort() {
208 qsort(events, internal_size, sizeof(kmp_stats_event), compare_two_events);
209}
210
211/* *********************************************************** */
212/* ************* kmp_stats_list member functions ************* */
213
214// returns a pointer to newly created stats node
215kmp_stats_list* kmp_stats_list::push_back(int gtid) {
216 kmp_stats_list* newnode = (kmp_stats_list*)__kmp_allocate(sizeof(kmp_stats_list));
217 // placement new, only requires space and pointer and initializes (so __kmp_allocate instead of C++ new[] is used)
218 new (newnode) kmp_stats_list();
219 newnode->setGtid(gtid);
220 newnode->prev = this->prev;
221 newnode->next = this;
222 newnode->prev->next = newnode;
223 newnode->next->prev = newnode;
224 return newnode;
225}
226void kmp_stats_list::deallocate() {
227 kmp_stats_list* ptr = this->next;
228 kmp_stats_list* delptr = this->next;
229 while(ptr != this) {
230 delptr = ptr;
231 ptr=ptr->next;
232 // placement new means we have to explicitly call destructor.
233 delptr->_event_vector.deallocate();
234 delptr->~kmp_stats_list();
235 __kmp_free(delptr);
236 }
237}
238kmp_stats_list::iterator kmp_stats_list::begin() {
239 kmp_stats_list::iterator it;
240 it.ptr = this->next;
241 return it;
242}
243kmp_stats_list::iterator kmp_stats_list::end() {
244 kmp_stats_list::iterator it;
245 it.ptr = this;
246 return it;
247}
248int kmp_stats_list::size() {
249 int retval;
250 kmp_stats_list::iterator it;
251 for(retval=0, it=begin(); it!=end(); it++, retval++) {}
252 return retval;
253}
254
255/* ********************************************************************* */
256/* ************* kmp_stats_list::iterator member functions ************* */
257
258kmp_stats_list::iterator::iterator() : ptr(NULL) {}
259kmp_stats_list::iterator::~iterator() {}
260kmp_stats_list::iterator kmp_stats_list::iterator::operator++() {
261 this->ptr = this->ptr->next;
262 return *this;
263}
264kmp_stats_list::iterator kmp_stats_list::iterator::operator++(int dummy) {
265 this->ptr = this->ptr->next;
266 return *this;
267}
268kmp_stats_list::iterator kmp_stats_list::iterator::operator--() {
269 this->ptr = this->ptr->prev;
270 return *this;
271}
272kmp_stats_list::iterator kmp_stats_list::iterator::operator--(int dummy) {
273 this->ptr = this->ptr->prev;
274 return *this;
275}
276bool kmp_stats_list::iterator::operator!=(const kmp_stats_list::iterator & rhs) {
277 return this->ptr!=rhs.ptr;
278}
279bool kmp_stats_list::iterator::operator==(const kmp_stats_list::iterator & rhs) {
280 return this->ptr==rhs.ptr;
281}
282kmp_stats_list* kmp_stats_list::iterator::operator*() const {
283 return this->ptr;
284}
285
286/* *************************************************************** */
287/* ************* kmp_stats_output_module functions ************** */
288
289const char* kmp_stats_output_module::outputFileName = NULL;
290const char* kmp_stats_output_module::eventsFileName = NULL;
291const char* kmp_stats_output_module::plotFileName = NULL;
292int kmp_stats_output_module::printPerThreadFlag = 0;
293int kmp_stats_output_module::printPerThreadEventsFlag = 0;
294
295// init() is called very near the beginning of execution time in the constructor of __kmp_stats_global_output
296void kmp_stats_output_module::init()
297{
298 char * statsFileName = getenv("KMP_STATS_FILE");
299 eventsFileName = getenv("KMP_STATS_EVENTS_FILE");
300 plotFileName = getenv("KMP_STATS_PLOT_FILE");
301 char * threadStats = getenv("KMP_STATS_THREADS");
302 char * threadEvents = getenv("KMP_STATS_EVENTS");
303
304 // set the stats output filenames based on environment variables and defaults
305 outputFileName = statsFileName;
306 eventsFileName = eventsFileName ? eventsFileName : "events.dat";
307 plotFileName = plotFileName ? plotFileName : "events.plt";
308
309 // set the flags based on environment variables matching: true, on, 1, .true. , .t. , yes
310 printPerThreadFlag = __kmp_str_match_true(threadStats);
311 printPerThreadEventsFlag = __kmp_str_match_true(threadEvents);
312
313 if(printPerThreadEventsFlag) {
314 // assigns a color to each timer for printing
315 setupEventColors();
316 } else {
317 // will clear flag so that no event will be logged
318 timeStat::clearEventFlags();
319 }
320
321 return;
322}
323
324void kmp_stats_output_module::setupEventColors() {
325 int i;
326 int globalColorIndex = 0;
327 int numGlobalColors = sizeof(globalColorArray) / sizeof(rgb_color);
328 for(i=0;i<TIMER_LAST;i++) {
329 if(timeStat::logEvent((timer_e)i)) {
330 timerColorInfo[i] = globalColorArray[globalColorIndex];
331 globalColorIndex = (globalColorIndex+1)%numGlobalColors;
332 }
333 }
334 return;
335}
336
Jonathan Peytone2554af2016-03-11 20:20:49 +0000337void kmp_stats_output_module::printTimerStats(FILE *statsOut, statistic const * theStats, statistic const * totalStats)
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000338{
Jonathan Peytone2554af2016-03-11 20:20:49 +0000339 fprintf (statsOut, "Timer, SampleCount, Min, Mean, Max, Total, SD\n");
340 for (int s = 0; s<TIMER_LAST; s++) {
341 statistic const * stat = &theStats[s];
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000342
Jonathan Peytone2554af2016-03-11 20:20:49 +0000343 char tag = timeStat::noUnits(timer_e(s)) ? ' ' : 'T';
344 fprintf (statsOut, "%-28s, %s\n", timeStat::name(timer_e(s)), stat->format(tag, true).c_str());
345 // Also print the Total_ versions of times.
346 if (totalStats && tag == 'T' && !timeStat::masterOnly(timer_e(s))) {
347 fprintf(statsOut, "Total_%-22s, %s\n", timeStat::name(timer_e(s)), totalStats[s].format(tag, true).c_str());
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000348 }
349 }
Jonathan Peytone2554af2016-03-11 20:20:49 +0000350}
351
352void kmp_stats_output_module::printCounterStats(FILE *statsOut, statistic const * theStats)
353{
354 fprintf (statsOut, "Counter, ThreadCount, Min, Mean, Max, Total, SD\n");
355 for (int s = 0; s<COUNTER_LAST; s++) {
356 statistic const * stat = &theStats[s];
357 fprintf (statsOut, "%-25s, %s\n", counter::name(counter_e(s)), stat->format(' ', true).c_str());
358 }
359}
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000360
361void kmp_stats_output_module::printCounters(FILE * statsOut, counter const * theCounters)
362{
363 // We print all the counters even if they are zero.
364 // That makes it easier to slice them into a spreadsheet if you need to.
365 fprintf (statsOut, "\nCounter, Count\n");
366 for (int c = 0; c<COUNTER_LAST; c++) {
367 counter const * stat = &theCounters[c];
368 fprintf (statsOut, "%-25s, %s\n", counter::name(counter_e(c)), formatSI(stat->getValue(), 9, ' ').c_str());
369 }
370}
371
372void kmp_stats_output_module::printEvents(FILE* eventsOut, kmp_stats_event_vector* theEvents, int gtid) {
373 // sort by start time before printing
374 theEvents->sort();
375 for (int i = 0; i < theEvents->size(); i++) {
376 kmp_stats_event ev = theEvents->at(i);
377 rgb_color color = getEventColor(ev.getTimerName());
378 fprintf(eventsOut, "%d %lu %lu %1.1f rgb(%1.1f,%1.1f,%1.1f) %s\n",
379 gtid,
380 ev.getStart(),
381 ev.getStop(),
382 1.2 - (ev.getNestLevel() * 0.2),
383 color.r, color.g, color.b,
384 timeStat::name(ev.getTimerName())
385 );
386 }
387 return;
388}
389
390void kmp_stats_output_module::windupExplicitTimers()
391{
392 // Wind up any explicit timers. We assume that it's fair at this point to just walk all the explcit timers in all threads
393 // and say "it's over".
394 // If the timer wasn't running, this won't record anything anyway.
395 kmp_stats_list::iterator it;
396 for(it = __kmp_stats_list.begin(); it != __kmp_stats_list.end(); it++) {
397 for (int timer=0; timer<EXPLICIT_TIMER_LAST; timer++) {
398 (*it)->getExplicitTimer(explicit_timer_e(timer))->stop((timer_e)timer);
399 }
400 }
401}
402
403void kmp_stats_output_module::printPloticusFile() {
404 int i;
405 int size = __kmp_stats_list.size();
406 FILE* plotOut = fopen(plotFileName, "w+");
407
408 fprintf(plotOut, "#proc page\n"
409 " pagesize: 15 10\n"
410 " scale: 1.0\n\n");
411
412 fprintf(plotOut, "#proc getdata\n"
413 " file: %s\n\n",
414 eventsFileName);
415
416 fprintf(plotOut, "#proc areadef\n"
417 " title: OpenMP Sampling Timeline\n"
418 " titledetails: align=center size=16\n"
419 " rectangle: 1 1 13 9\n"
420 " xautorange: datafield=2,3\n"
421 " yautorange: -1 %d\n\n",
422 size);
423
424 fprintf(plotOut, "#proc xaxis\n"
425 " stubs: inc\n"
426 " stubdetails: size=12\n"
427 " label: Time (ticks)\n"
428 " labeldetails: size=14\n\n");
429
430 fprintf(plotOut, "#proc yaxis\n"
431 " stubs: inc 1\n"
432 " stubrange: 0 %d\n"
433 " stubdetails: size=12\n"
434 " label: Thread #\n"
435 " labeldetails: size=14\n\n",
436 size-1);
437
438 fprintf(plotOut, "#proc bars\n"
439 " exactcolorfield: 5\n"
440 " axis: x\n"
441 " locfield: 1\n"
442 " segmentfields: 2 3\n"
443 " barwidthfield: 4\n\n");
444
445 // create legend entries corresponding to the timer color
446 for(i=0;i<TIMER_LAST;i++) {
447 if(timeStat::logEvent((timer_e)i)) {
448 rgb_color c = getEventColor((timer_e)i);
449 fprintf(plotOut, "#proc legendentry\n"
450 " sampletype: color\n"
451 " label: %s\n"
452 " details: rgb(%1.1f,%1.1f,%1.1f)\n\n",
453 timeStat::name((timer_e)i),
454 c.r, c.g, c.b);
455
456 }
457 }
458
459 fprintf(plotOut, "#proc legend\n"
460 " format: down\n"
461 " location: max max\n\n");
462 fclose(plotOut);
463 return;
464}
465
466void kmp_stats_output_module::outputStats(const char* heading)
467{
468 statistic allStats[TIMER_LAST];
Jonathan Peytone2554af2016-03-11 20:20:49 +0000469 statistic totalStats[TIMER_LAST]; /* Synthesized, cross threads versions of normal timer stats */
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000470 statistic allCounters[COUNTER_LAST];
471
472 // stop all the explicit timers for all threads
473 windupExplicitTimers();
474
475 FILE * eventsOut;
476 FILE * statsOut = outputFileName ? fopen (outputFileName, "a+") : stderr;
477
478 if (eventPrintingEnabled()) {
479 eventsOut = fopen(eventsFileName, "w+");
480 }
481
482 if (!statsOut)
483 statsOut = stderr;
484
485 fprintf(statsOut, "%s\n",heading);
486 // Accumulate across threads.
487 kmp_stats_list::iterator it;
488 for (it = __kmp_stats_list.begin(); it != __kmp_stats_list.end(); it++) {
489 int t = (*it)->getGtid();
490 // Output per thread stats if requested.
491 if (perThreadPrintingEnabled()) {
492 fprintf (statsOut, "Thread %d\n", t);
Jonathan Peytone2554af2016-03-11 20:20:49 +0000493 printTimerStats(statsOut, (*it)->getTimers(), 0);
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000494 printCounters(statsOut, (*it)->getCounters());
495 fprintf(statsOut,"\n");
496 }
497 // Output per thread events if requested.
498 if (eventPrintingEnabled()) {
499 kmp_stats_event_vector events = (*it)->getEventVector();
500 printEvents(eventsOut, &events, t);
501 }
502
Jonathan Peytone2554af2016-03-11 20:20:49 +0000503 // Accumulate timers.
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000504 for (int s = 0; s<TIMER_LAST; s++) {
505 // See if we should ignore this timer when aggregating
506 if ((timeStat::masterOnly(timer_e(s)) && (t != 0)) || // Timer is only valid on the master and this thread is a worker
Jonathan Peytone2554af2016-03-11 20:20:49 +0000507 (timeStat::workerOnly(timer_e(s)) && (t == 0)) // Timer is only valid on a worker and this thread is the master
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000508 )
509 {
510 continue;
511 }
512
513 statistic * threadStat = (*it)->getTimer(timer_e(s));
514 allStats[s] += *threadStat;
Jonathan Peytone2554af2016-03-11 20:20:49 +0000515
516 // Add Total stats for all real times (not counts) that are valid in more than one thread
517 if (!timeStat::noUnits(timer_e(s)) && !timeStat::masterOnly(timer_e(s)))
518 {
519 totalStats[s].addSample(threadStat->getTotal());
520 }
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000521 }
522
Jonathan Peytone2554af2016-03-11 20:20:49 +0000523 // Accumulate counters.
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000524 for (int c = 0; c<COUNTER_LAST; c++) {
525 if (counter::masterOnly(counter_e(c)) && t != 0)
526 continue;
527 allCounters[c].addSample ((*it)->getCounter(counter_e(c))->getValue());
528 }
529 }
530
531 if (eventPrintingEnabled()) {
532 printPloticusFile();
533 fclose(eventsOut);
534 }
535
536 fprintf (statsOut, "Aggregate for all threads\n");
Jonathan Peytone2554af2016-03-11 20:20:49 +0000537 printTimerStats (statsOut, &allStats[0], &totalStats[0]);
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000538 fprintf (statsOut, "\n");
Jonathan Peytone2554af2016-03-11 20:20:49 +0000539 printCounterStats (statsOut, &allCounters[0]);
Jim Cownie4cc4bb42014-10-07 16:25:50 +0000540
541 if (statsOut != stderr)
542 fclose(statsOut);
543
544}
545
546/* ************************************************** */
547/* ************* exported C functions ************** */
548
549// no name mangling for these functions, we want the c files to be able to get at these functions
550extern "C" {
551
552void __kmp_reset_stats()
553{
554 kmp_stats_list::iterator it;
555 for(it = __kmp_stats_list.begin(); it != __kmp_stats_list.end(); it++) {
556 timeStat * timers = (*it)->getTimers();
557 counter * counters = (*it)->getCounters();
558 explicitTimer * eTimers = (*it)->getExplicitTimers();
559
560 for (int t = 0; t<TIMER_LAST; t++)
561 timers[t].reset();
562
563 for (int c = 0; c<COUNTER_LAST; c++)
564 counters[c].reset();
565
566 for (int t=0; t<EXPLICIT_TIMER_LAST; t++)
567 eTimers[t].reset();
568
569 // reset the event vector so all previous events are "erased"
570 (*it)->resetEventVector();
571
572 // May need to restart the explicit timers in thread zero?
573 }
574 KMP_START_EXPLICIT_TIMER(OMP_serial);
575 KMP_START_EXPLICIT_TIMER(OMP_start_end);
576}
577
578// This function will reset all stats and stop all threads' explicit timers if they haven't been stopped already.
579void __kmp_output_stats(const char * heading)
580{
581 __kmp_stats_global_output.outputStats(heading);
582 __kmp_reset_stats();
583}
584
585void __kmp_accumulate_stats_at_exit(void)
586{
587 // Only do this once.
588 if (KMP_XCHG_FIXED32(&statsPrinted, 1) != 0)
589 return;
590
591 __kmp_output_stats("Statistics on exit");
592 return;
593}
594
595void __kmp_stats_init(void)
596{
597 return;
598}
599
600} // extern "C"
601