Skip to content

Commit ecfc5d4

Browse files
committed
Updated benchmark
1 parent ed9f154 commit ecfc5d4

2 files changed

Lines changed: 262 additions & 29 deletions

File tree

examples/benchmark/README.md

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ A C++ benchmarking tool that compares the decoding performance of [Dynamsoft Bar
55
## Features
66

77
- Processes single images or entire directories (recursive)
8-
- Runs each library multiple times per image (default: 10) and reports avg/min/max decode time
9-
- Tabular per-image comparison and overall summary
8+
- Single-thread benchmark: runs each library multiple times per image and reports avg/min/max decode time
9+
- Multi-thread benchmark (optional): processes all images concurrently across N threads, reports throughput and wall-clock time
10+
- Tabular per-image single-thread comparison + overall summary
1011
- Supports common image formats: JPG, PNG, BMP, TIFF, GIF, PDF (Dynamsoft only)
1112
- Cross-platform CMake build (Windows, Linux, macOS)
1213

@@ -56,9 +57,10 @@ After a successful build the executable is at `build/Release/benchmark.exe` (Win
5657
## Usage
5758

5859
```bash
59-
benchmark <image_or_folder> [iterations]
60+
benchmark [--threads N] <image_or_folder> [iterations]
6061
```
6162

63+
- `--threads N` — (optional) run multi-threaded throughput benchmark with N threads after single-thread comparison.
6264
- `image_or_folder` — path to a single image file or a directory (scanned recursively).
6365
- `iterations` — number of decoding passes per image (default: 10). Higher values give more stable timing averages.
6466

@@ -76,32 +78,37 @@ Benchmark a folder with 5 iterations:
7678
benchmark "C:\images" 5
7779
```
7880

79-
### Sample Output
81+
Benchmark a folder with 5 iterations and 4 threads (multi-thread throughput):
8082

83+
```bash
84+
benchmark --threads 4 "C:\images" 5
8185
```
82-
=============================================================
83-
Barcode Benchmark: Dynamsoft DBR v11.4.20.7177 vs zxing-cpp v3.0.2
84-
=============================================================
8586

86-
Found 1 image(s). Iterations per image: 3
87+
### Sample Output (multi-threaded)
8788

88-
--- Code 128.png ---
89-
| Library | Avg (ms)| Min (ms)| Max (ms)| Barcodes|
90-
|------------|--------------|--------------|--------------|--------------|
91-
| Dynamsoft | 36.21| 17.62| 71.48| 1|
92-
| zxing-cpp | 9.88| 9.55| 10.26| 1|
89+
With `--threads 4`, the multi-thread benchmark section is appended after the single-thread summary:
9390

91+
```
9492
=============================================================
95-
Summary (1 image(s), 3 iterations each)
93+
Single-Thread Summary (44 image(s), 3 iterations each)
9694
=============================================================
9795
| Library | Total Avg| Total Bc|
9896
|------------|--------------|--------------|
99-
| Dynamsoft | 36.21ms| 1|
100-
| zxing-cpp | 9.88ms| 1|
97+
| Dynamsoft | 3326.84ms| 353|
98+
| zxing-cpp | 1960.86ms| 232|
99+
100+
=============================================================
101+
--- Multi-Threaded Benchmark (4 threads) ---
102+
| Library | Wall Time (s)| Decodes/s| Total Decodes| Barcodes|
103+
|------------|--------------|--------------|--------------|--------------|
104+
| Dynamsoft | 3.726| 35.4| 132| 353|
105+
| zxing-cpp | 2.368| 54.5| 129| 232|
106+
Speedup: Dynamsoft 2.93x, zxing-cpp 2.53x
101107
```
102108

103109
## Notes
104110

105111
- **PDF files**: Only Dynamsoft can process PDFs. zxing-cpp (via stb_image) does not support PDF input — those rows show "N/A".
106112
- **License**: The Dynamsoft trial license key is embedded in the source. Replace it with your own license for production use.
107113
- **Fair comparison**: zxing-cpp decodes from pre-loaded pixel data while Dynamsoft reads from file each iteration (its native API). Both timings include the full decoding pipeline.
114+
- **Multi-threading**: Each thread creates its own Dynamsoft `CCaptureVisionRouter` instance since the SDK is not thread-safe for concurrent decode calls on a single router. zxing-cpp's `ReadBarcodes` is thread-safe and each thread loads its own image data independently.

examples/benchmark/main.cpp

Lines changed: 239 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/*
22
* Barcode Benchmark: Dynamsoft Capture Vision vs zxing-cpp
33
*
4-
* Usage: benchmark <image_or_folder> [iterations]
4+
* Usage: benchmark [--threads N] <image_or_folder> [iterations]
55
* image_or_folder : path to a single image file or a directory of images
66
* iterations : number of decoding passes per image (default: 10)
7+
* --threads N : run multi-threaded throughput benchmark with N threads
78
*/
89

910
#include <iostream>
@@ -16,6 +17,9 @@
1617
#include <cstdio>
1718
#include <cstring>
1819
#include <numeric>
20+
#include <thread>
21+
#include <mutex>
22+
#include <atomic>
1923

2024
// Prevent Windows macros from conflicting with std::min/std::max
2125
#ifdef _WIN32
@@ -194,6 +198,143 @@ static BenchResult benchZXing(const string& filePath, int iterations)
194198
return res;
195199
}
196200

201+
// ---------------------------------------------------------------------------
202+
// Multi-threading types
203+
// ---------------------------------------------------------------------------
204+
struct MultiThreadResult
205+
{
206+
string libraryName;
207+
int totalBarcodes = 0;
208+
double wallTimeSec = 0.0;
209+
double throughput = 0.0;
210+
int totalDecodes = 0;
211+
};
212+
213+
// ---------------------------------------------------------------------------
214+
// Dynamsoft multi-thread benchmark
215+
// ---------------------------------------------------------------------------
216+
static MultiThreadResult benchDynamsoftMulti(CCaptureVisionRouter* cvr,
217+
const vector<string>& images,
218+
int iterations, int numThreads)
219+
{
220+
(void)cvr; // unused in this implementation (each thread creates its own router)
221+
MultiThreadResult res;
222+
res.libraryName = "Dynamsoft";
223+
224+
auto t0 = high_resolution_clock::now();
225+
atomic<int> totalBarcodes{0};
226+
vector<thread> workers;
227+
228+
for (int t = 0; t < numThreads; ++t)
229+
{
230+
workers.emplace_back([&, t]() {
231+
CCaptureVisionRouter* localCvr = new CCaptureVisionRouter;
232+
char err[512] = {0};
233+
localCvr->InitSettingsFromFile("Templates/DBR-PresetTemplates.json", err, 512);
234+
235+
int localBc = 0;
236+
for (size_t i = t; i < images.size(); i += numThreads)
237+
{
238+
for (int it = 0; it < iterations; ++it)
239+
{
240+
CCapturedResultArray* arr = localCvr->CaptureMultiPages(
241+
images[i].c_str(), CPresetTemplate::PT_READ_BARCODES);
242+
243+
int count = 0;
244+
int pageCount = arr->GetResultsCount();
245+
for (int p = 0; p < pageCount; ++p)
246+
{
247+
const CCapturedResult* r = arr->GetResult(p);
248+
if (r->GetErrorCode() != 0) continue;
249+
CDecodedBarcodesResult* br = r->GetDecodedBarcodesResult();
250+
if (br && br->GetErrorCode() == 0)
251+
count += br->GetItemsCount();
252+
if (br) br->Release();
253+
}
254+
arr->Release();
255+
if (it == 0) localBc = count;
256+
}
257+
totalBarcodes.fetch_add(localBc);
258+
}
259+
delete localCvr;
260+
});
261+
}
262+
for (auto& w : workers) w.join();
263+
264+
auto t1 = high_resolution_clock::now();
265+
res.wallTimeSec = duration_cast<microseconds>(t1 - t0).count() / 1e6;
266+
res.totalBarcodes = totalBarcodes.load();
267+
res.totalDecodes = static_cast<int>(images.size()) * iterations;
268+
res.throughput = res.totalDecodes / res.wallTimeSec;
269+
return res;
270+
}
271+
272+
// ---------------------------------------------------------------------------
273+
// zxing-cpp multi-thread benchmark
274+
// ---------------------------------------------------------------------------
275+
static MultiThreadResult benchZXingMulti(const vector<string>& images,
276+
int iterations, int numThreads)
277+
{
278+
MultiThreadResult res;
279+
res.libraryName = "zxing-cpp";
280+
281+
// Filter out PDFs (stb_image cannot load them)
282+
vector<string> validImages;
283+
for (const auto& img : images) {
284+
string ext = filesystem::path(img).extension().string();
285+
transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
286+
if (ext != ".pdf")
287+
validImages.push_back(img);
288+
}
289+
if (validImages.empty()) return res;
290+
291+
ZXing::ReaderOptions options;
292+
options.tryHarder(true);
293+
static const array<ZXing::ImageFormat, 5> fmtMap = {
294+
ZXing::ImageFormat::None, ZXing::ImageFormat::Lum,
295+
ZXing::ImageFormat::LumA, ZXing::ImageFormat::RGB,
296+
ZXing::ImageFormat::RGBA
297+
};
298+
299+
auto t0 = high_resolution_clock::now();
300+
atomic<int> totalBarcodes{0};
301+
vector<thread> workers;
302+
303+
for (int t = 0; t < numThreads; ++t)
304+
{
305+
workers.emplace_back([&, t]() {
306+
for (size_t i = t; i < validImages.size(); i += numThreads)
307+
{
308+
int width = 0, height = 0, channels = 0;
309+
unsigned char* buf = stbi_load(validImages[i].c_str(), &width, &height, &channels, 0);
310+
if (!buf) continue;
311+
312+
ZXing::ImageFormat fmt = (channels >= 1 && channels <= 4)
313+
? fmtMap[channels]
314+
: ZXing::ImageFormat::None;
315+
ZXing::ImageView iv(buf, width, height, fmt);
316+
317+
int localBc = 0;
318+
for (int it = 0; it < iterations; ++it)
319+
{
320+
auto bc = ZXing::ReadBarcodes(iv, options);
321+
if (it == 0) localBc = static_cast<int>(bc.size());
322+
}
323+
totalBarcodes.fetch_add(localBc);
324+
stbi_image_free(buf);
325+
}
326+
});
327+
}
328+
for (auto& w : workers) w.join();
329+
330+
auto t1 = high_resolution_clock::now();
331+
res.wallTimeSec = duration_cast<microseconds>(t1 - t0).count() / 1e6;
332+
res.totalBarcodes = totalBarcodes.load();
333+
res.totalDecodes = static_cast<int>(validImages.size()) * iterations;
334+
res.throughput = res.totalDecodes / res.wallTimeSec;
335+
return res;
336+
}
337+
197338
// ---------------------------------------------------------------------------
198339
// Print helpers
199340
// ---------------------------------------------------------------------------
@@ -227,6 +368,33 @@ static void printRow(const BenchResult& r)
227368
<< "|\n";
228369
}
229370

371+
// ---------------------------------------------------------------------------
372+
// Multi-thread print helpers
373+
// ---------------------------------------------------------------------------
374+
static void printMultiHeader(int numThreads)
375+
{
376+
cout << "--- Multi-Threaded Benchmark (" << numThreads << " threads) ---\n";
377+
cout << "|" << left << setw(12) << " Library"
378+
<< "|" << right << setw(14) << " Wall Time (s)"
379+
<< "|" << right << setw(14) << " Decodes/s"
380+
<< "|" << right << setw(14) << " Total Decodes"
381+
<< "|" << right << setw(14) << " Barcodes"
382+
<< "|\n";
383+
cout << "|" << string(12, '-') << "|" << string(14, '-')
384+
<< "|" << string(14, '-') << "|" << string(14, '-')
385+
<< "|" << string(14, '-') << "|\n";
386+
}
387+
388+
static void printMultiRow(const MultiThreadResult& r)
389+
{
390+
cout << "|" << left << setw(12) << (" " + r.libraryName)
391+
<< "|" << right << setw(14) << fixed << setprecision(3) << r.wallTimeSec
392+
<< "|" << right << setw(14) << fixed << setprecision(1) << r.throughput
393+
<< "|" << right << setw(14) << r.totalDecodes
394+
<< "|" << right << setw(14) << r.totalBarcodes
395+
<< "|\n";
396+
}
397+
230398
// ---------------------------------------------------------------------------
231399
// main
232400
// ---------------------------------------------------------------------------
@@ -239,17 +407,40 @@ int main(int argc, char* argv[])
239407
<< " vs zxing-cpp v" << ZXING_VERSION << "\n";
240408
cout << "=============================================================\n\n";
241409

242-
if (argc < 2) {
410+
// --- Parse arguments ---
411+
int numThreads = 0;
412+
int iterations = 10;
413+
string inputPath;
414+
415+
vector<string> posArgs;
416+
for (int i = 1; i < argc; ++i) {
417+
if (strcmp(argv[i], "--threads") == 0) {
418+
if (i + 1 < argc) {
419+
numThreads = atoi(argv[++i]);
420+
if (numThreads < 1) numThreads = 1;
421+
} else {
422+
cerr << "Error: --threads requires a number argument\n";
423+
return 1;
424+
}
425+
} else {
426+
posArgs.push_back(argv[i]);
427+
}
428+
}
429+
430+
if (posArgs.empty()) {
243431
cout << "Usage: " << (argc > 0 ? argv[0] : "benchmark")
244-
<< " <image_or_folder> [iterations]\n"
432+
<< " [--threads N] <image_or_folder> [iterations]\n"
245433
<< " image_or_folder : path to an image file or a directory\n"
246-
<< " iterations : decoding passes per image (default: 10)\n";
434+
<< " iterations : decoding passes per image (default: 10)\n"
435+
<< " --threads N : run multi-threaded throughput benchmark with N threads\n";
247436
return 0;
248437
}
249438

250-
string inputPath = argv[1];
251-
int iterations = (argc >= 3) ? atoi(argv[2]) : 10;
252-
if (iterations < 1) iterations = 1;
439+
inputPath = posArgs[0];
440+
if (posArgs.size() >= 2) {
441+
iterations = atoi(posArgs[1].c_str());
442+
if (iterations < 1) iterations = 1;
443+
}
253444

254445
if (!filesystem::exists(inputPath)) {
255446
cerr << "Error: path does not exist: " << inputPath << "\n";
@@ -262,10 +453,13 @@ int main(int argc, char* argv[])
262453
return 1;
263454
}
264455

265-
cout << "Found " << images.size() << " image(s). Iterations per image: "
266-
<< iterations << "\n\n";
456+
cout << "Found " << images.size() << " image(s). "
457+
<< "Iterations per image: " << iterations;
458+
if (numThreads > 0)
459+
cout << ", Threads: " << numThreads;
460+
cout << "\n\n";
267461

268-
// Initialize Dynamsoft
462+
// --- Initialize Dynamsoft ---
269463
char szErrorMsg[256] = {0};
270464
int iRet = CLicenseManager::InitLicense(
271465
"DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==",
@@ -284,7 +478,9 @@ int main(int argc, char* argv[])
284478
return 1;
285479
}
286480

287-
// Accumulators for overall summary
481+
// ================================================================
482+
// Single-thread benchmark (per-image detail)
483+
// ================================================================
288484
double totalDynTime = 0.0;
289485
int totalDynBc = 0;
290486
double totalZxTime = 0.0;
@@ -326,9 +522,9 @@ int main(int argc, char* argv[])
326522
cout << "\n";
327523
}
328524

329-
// Summary
525+
// Single-thread summary
330526
cout << "=============================================================\n";
331-
cout << " Summary (" << processedCount << " image(s), "
527+
cout << " Single-Thread Summary (" << processedCount << " image(s), "
332528
<< iterations << " iterations each)\n";
333529
cout << "=============================================================\n";
334530
cout << "|" << left << setw(12) << " Library"
@@ -347,6 +543,36 @@ int main(int argc, char* argv[])
347543
<< "|" << right << setw(14) << totalZxBc << "|\n";
348544
cout << "\n";
349545

546+
// ================================================================
547+
// Multi-thread benchmark (optional)
548+
// ================================================================
549+
if (numThreads > 0)
550+
{
551+
cout << "=============================================================\n";
552+
printMultiHeader(numThreads);
553+
554+
MultiThreadResult dynMulti = benchDynamsoftMulti(cvr, images, iterations, numThreads);
555+
printMultiRow(dynMulti);
556+
557+
MultiThreadResult zxMulti = benchZXingMulti(images, iterations, numThreads);
558+
printMultiRow(zxMulti);
559+
560+
// Speedup vs single-thread
561+
if (totalDynTime > 0 && dynMulti.wallTimeSec > 0) {
562+
double dynSeqTotal = totalDynTime * iterations / 1000.0;
563+
double dynSpeedup = dynSeqTotal / dynMulti.wallTimeSec;
564+
double zxSeqTime = totalZxTime / 1000.0;
565+
if (zxSeqTime > 0 && zxMulti.wallTimeSec > 0) {
566+
double zxSeqTotal = zxSeqTime * iterations;
567+
double zxSpeedup = zxSeqTotal / zxMulti.wallTimeSec;
568+
cout << fixed << setprecision(2);
569+
cout << "Speedup: Dynamsoft " << dynSpeedup << "x, zxing-cpp "
570+
<< zxSpeedup << "x" << std::endl;
571+
}
572+
}
573+
cout << "\n";
574+
}
575+
350576
delete cvr;
351577
return 0;
352578
}

0 commit comments

Comments
 (0)