Skip to content

Commit 54f1fbf

Browse files
committed
ENH: Add benchmark for iterator loop comparison
1 parent 7f3c0d9 commit 54f1fbf

File tree

2 files changed

+389
-0
lines changed

2 files changed

+389
-0
lines changed

examples/Core/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,14 @@ add_test(
1818
set_property(TEST ThreadOverheadBenchmark APPEND PROPERTY LABELS Core)
1919
## performance tests should not be run in parallel
2020
set_tests_properties(ThreadOverheadBenchmark PROPERTIES RUN_SERIAL TRUE)
21+
22+
add_executable(ImageIterationBenchmark itkImageIterationBenchmark.cxx )
23+
target_link_libraries(ImageIterationBenchmark ${ITK_LIBRARIES})
24+
add_test(
25+
NAME ImageIterationBenchmark
26+
COMMAND ImageIterationBenchmark
27+
${BENCHMARK_RESULTS_OUTPUT_DIR}/__DATESTAMP__ImageIterationBenchmark.json
28+
50 128 )
29+
set_property(TEST ImageIterationBenchmark APPEND PROPERTY LABELS Core)
30+
## performance tests should not be run in parallel
31+
set_tests_properties(ImageIterationBenchmark PROPERTIES RUN_SERIAL TRUE)
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
/*=========================================================================
2+
*
3+
* Copyright NumFOCUS
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0.txt
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*=========================================================================*/
18+
19+
#include <iostream>
20+
#include "itkImage.h"
21+
#include "itkVectorImage.h"
22+
#include "itkVector.h"
23+
#include "itkRGBPixel.h"
24+
#include "itkImageRegionRange.h"
25+
#include "itkImageScanlineIterator.h"
26+
#include "itkImageScanlineConstIterator.h"
27+
#include "itkImageRegionIteratorWithIndex.h"
28+
#include "itkImageRegionConstIterator.h"
29+
#include "itkHighPriorityRealTimeProbesCollector.h"
30+
#include "PerformanceBenchmarkingUtilities.h"
31+
#include "itkNumericTraits.h"
32+
#include <iomanip>
33+
#include <fstream>
34+
35+
36+
// Helper function to initialize an image with random values
37+
template <typename TImage>
38+
typename TImage::Pointer
39+
CreateAndInitializeImage(const typename TImage::SizeType & size, unsigned int numberOfComponentsPerPixel = 0)
40+
{
41+
auto image = TImage::New();
42+
typename TImage::RegionType region{ size };
43+
image->SetRegions(region);
44+
if (numberOfComponentsPerPixel > 0)
45+
{
46+
image->SetNumberOfComponentsPerPixel(numberOfComponentsPerPixel);
47+
}
48+
image->Allocate();
49+
50+
// Initialize with simple pattern (pixel index-based)
51+
using PixelType = typename TImage::PixelType;
52+
unsigned int count = 0;
53+
54+
const unsigned int length = image->GetNumberOfComponentsPerPixel();
55+
56+
itk::ImageRegionIteratorWithIndex<TImage> it(image, region);
57+
for (it.GoToBegin(); !it.IsAtEnd(); ++it)
58+
{
59+
PixelType pixel{ it.Get() };
60+
for (unsigned int k = 0; k < length; ++k)
61+
{
62+
pixel[k] = static_cast<typename itk::NumericTraits<PixelType>::ValueType>(count + k);
63+
}
64+
it.Set(pixel);
65+
++count;
66+
}
67+
68+
return image;
69+
}
70+
71+
72+
// Method 1: ImageScanlineIterator approach
73+
template <typename TInputImage, typename TOutputImage>
74+
void
75+
CopyScanlineIterator(const TInputImage * inputPtr, TOutputImage * outputPtr)
76+
{
77+
using InputPixelType = typename TInputImage::PixelType;
78+
using OutputPixelType = typename TOutputImage::PixelType;
79+
using ImageScanlineConstIterator = itk::ImageScanlineConstIterator<TInputImage>;
80+
using ImageScanlineIterator = itk::ImageScanlineIterator<TOutputImage>;
81+
82+
const typename TOutputImage::RegionType outputRegion = outputPtr->GetRequestedRegion();
83+
typename TInputImage::RegionType inputRegion = outputRegion;
84+
85+
ImageScanlineConstIterator inputIt(inputPtr, inputRegion);
86+
ImageScanlineIterator outputIt(outputPtr, outputRegion);
87+
88+
const unsigned int componentsPerPixel = inputPtr->GetNumberOfComponentsPerPixel();
89+
while (!inputIt.IsAtEnd())
90+
{
91+
while (!inputIt.IsAtEndOfLine())
92+
{
93+
const InputPixelType & inputPixel = inputIt.Get();
94+
OutputPixelType value(outputIt.Get());
95+
for (unsigned int k = 0; k < componentsPerPixel; ++k)
96+
{
97+
value[k] = static_cast<typename OutputPixelType::ValueType>(inputPixel[k]);
98+
}
99+
outputIt.Set(value);
100+
101+
++inputIt;
102+
++outputIt;
103+
}
104+
inputIt.NextLine();
105+
outputIt.NextLine();
106+
}
107+
}
108+
109+
110+
// Method 1b: ImageScanlineIterator approach using NumericTraits::GetLength()
111+
template <typename TInputImage, typename TOutputImage>
112+
void
113+
CopyScanlineIteratorNumericTraits(const TInputImage * inputPtr, TOutputImage * outputPtr)
114+
{
115+
using InputPixelType = typename TInputImage::PixelType;
116+
using OutputPixelType = typename TOutputImage::PixelType;
117+
using ImageScanlineConstIterator = itk::ImageScanlineConstIterator<TInputImage>;
118+
using ImageScanlineIterator = itk::ImageScanlineIterator<TOutputImage>;
119+
120+
const typename TOutputImage::RegionType outputRegion = outputPtr->GetRequestedRegion();
121+
typename TInputImage::RegionType inputRegion = outputRegion;
122+
123+
ImageScanlineConstIterator inputIt(inputPtr, inputRegion);
124+
ImageScanlineIterator outputIt(outputPtr, outputRegion);
125+
126+
unsigned int componentsPerPixel = itk::NumericTraits<OutputPixelType>::GetLength(outputIt.Get());
127+
while (!inputIt.IsAtEnd())
128+
{
129+
while (!inputIt.IsAtEndOfLine())
130+
{
131+
const InputPixelType & inputPixel = inputIt.Get();
132+
133+
OutputPixelType value{ outputIt.Get() };
134+
for (unsigned int k = 0; k < componentsPerPixel; ++k)
135+
{
136+
value[k] = static_cast<typename OutputPixelType::ValueType>(inputPixel[k]);
137+
}
138+
outputIt.Set(value);
139+
140+
++inputIt;
141+
++outputIt;
142+
}
143+
inputIt.NextLine();
144+
outputIt.NextLine();
145+
}
146+
}
147+
148+
149+
// Method 2: ImageRegionRange approach
150+
template <typename TInputImage, typename TOutputImage>
151+
void
152+
CopyImageRegionRange(const TInputImage * inputPtr, TOutputImage * outputPtr)
153+
{
154+
using InputPixelType = typename TInputImage::PixelType;
155+
using OutputPixelType = typename TOutputImage::PixelType;
156+
157+
const typename TOutputImage::RegionType outputRegion = outputPtr->GetRequestedRegion();
158+
typename TInputImage::RegionType inputRegion = outputRegion;
159+
160+
auto inputRange = itk::ImageRegionRange<const TInputImage>(*inputPtr, inputRegion);
161+
auto outputRange = itk::ImageRegionRange<TOutputImage>(*outputPtr, outputRegion);
162+
163+
auto inputIt = inputRange.begin();
164+
auto outputIt = outputRange.begin();
165+
const auto inputEnd = inputRange.end();
166+
167+
const unsigned int componentsPerPixel = inputPtr->GetNumberOfComponentsPerPixel();
168+
while (inputIt != inputEnd)
169+
{
170+
const InputPixelType & inputPixel = *inputIt;
171+
OutputPixelType outputPixel{ *outputIt };
172+
for (unsigned int k = 0; k < componentsPerPixel; ++k)
173+
{
174+
outputPixel[k] = static_cast<typename OutputPixelType::ValueType>(inputPixel[k]);
175+
}
176+
*outputIt = outputPixel;
177+
178+
++inputIt;
179+
++outputIt;
180+
}
181+
}
182+
183+
184+
// Method 2b: ImageRegionRange approach using NumericTraits::GetLength()
185+
template <typename TInputImage, typename TOutputImage>
186+
void
187+
CopyImageRegionRangeNumericTraits(const TInputImage * inputPtr, TOutputImage * outputPtr)
188+
{
189+
using InputPixelType = typename TInputImage::PixelType;
190+
using OutputPixelType = typename TOutputImage::PixelType;
191+
192+
const typename TOutputImage::RegionType outputRegion = outputPtr->GetRequestedRegion();
193+
typename TInputImage::RegionType inputRegion = outputRegion;
194+
195+
auto inputRange = itk::ImageRegionRange<const TInputImage>(*inputPtr, inputRegion);
196+
auto outputRange = itk::ImageRegionRange<TOutputImage>(*outputPtr, outputRegion);
197+
198+
auto inputIt = inputRange.begin();
199+
auto outputIt = outputRange.begin();
200+
const auto inputEnd = inputRange.end();
201+
202+
const unsigned int componentsPerPixel = itk::NumericTraits<OutputPixelType>::GetLength(*outputIt);
203+
while (inputIt != inputEnd)
204+
{
205+
const InputPixelType & inputPixel = *inputIt;
206+
OutputPixelType outputPixel{ *outputIt };
207+
for (unsigned int k = 0; k < componentsPerPixel; ++k)
208+
{
209+
outputPixel[k] = static_cast<typename OutputPixelType::ValueType>(inputPixel[k]);
210+
}
211+
*outputIt = outputPixel;
212+
++inputIt;
213+
++outputIt;
214+
}
215+
}
216+
217+
218+
// Method 2c: ImageRegionRange approach - BUGGY VERSION demonstrating incorrect pixel reuse
219+
// NOTE: This method has a BUG - outputPixel is initialized once and reused, causing incorrect results
220+
// when the pixel type holds a reference to the image buffer (modifies first pixel repeatedly)
221+
template <typename TInputImage, typename TOutputImage>
222+
void
223+
CopyImageRegionRangeNumericTraitsAsRange(const TInputImage * inputPtr, TOutputImage * outputPtr)
224+
{
225+
using InputPixelType = typename TInputImage::PixelType;
226+
using OutputPixelType = typename TOutputImage::PixelType;
227+
228+
const typename TOutputImage::RegionType outputRegion = outputPtr->GetRequestedRegion();
229+
typename TInputImage::RegionType inputRegion = outputRegion;
230+
231+
auto outputRange = itk::ImageRegionRange<TOutputImage>(*outputPtr, outputRegion);
232+
auto outputIt = outputRange.begin();
233+
234+
const unsigned int componentsPerPixel = itk::NumericTraits<OutputPixelType>::GetLength(*outputIt);
235+
OutputPixelType outputPixel{ *outputIt };
236+
for (const InputPixelType & inputPixel : itk::ImageRegionRange<const TInputImage>(*inputPtr, inputRegion))
237+
{
238+
for (unsigned int k = 0; k < componentsPerPixel; ++k)
239+
{
240+
outputPixel[k] = static_cast<typename OutputPixelType::ValueType>(inputPixel[k]);
241+
}
242+
*outputIt = outputPixel;
243+
++outputIt;
244+
}
245+
}
246+
247+
248+
// Helper function to time a single method
249+
template <typename TInputImage, typename TOutputImage, typename TCopyFunction>
250+
void
251+
TimeMethod(itk::HighPriorityRealTimeProbesCollector & collector,
252+
const std::string & methodName,
253+
TCopyFunction copyFunc,
254+
const TInputImage * inputImage,
255+
typename TOutputImage::Pointer & outputImage,
256+
int iterations)
257+
{
258+
// Allocate output image
259+
outputImage = TOutputImage::New();
260+
outputImage->SetRegions(inputImage->GetLargestPossibleRegion());
261+
outputImage->SetNumberOfComponentsPerPixel(inputImage->GetNumberOfComponentsPerPixel());
262+
outputImage->Allocate();
263+
264+
// Warm-up run
265+
copyFunc(inputImage, outputImage.GetPointer());
266+
267+
// Timed runs
268+
for (int ii = 0; ii < iterations; ++ii)
269+
{
270+
collector.Start(methodName.c_str());
271+
copyFunc(inputImage, outputImage.GetPointer());
272+
collector.Stop(methodName.c_str());
273+
}
274+
275+
}
276+
277+
278+
// Performance testing function
279+
template <typename TInputImage, typename TOutputImage>
280+
void
281+
TimeIterationMethods(itk::HighPriorityRealTimeProbesCollector & collector,
282+
const typename TInputImage::SizeType & size,
283+
const std::string & description,
284+
int iterations)
285+
{
286+
287+
// Create and initialize input image
288+
auto inputImage = CreateAndInitializeImage<TInputImage>(size, 3);
289+
290+
typename TOutputImage::Pointer referenceImage;
291+
typename TOutputImage::Pointer outputImage;
292+
293+
// Test Method 1: Scanline Iterator - serves as reference
294+
TimeMethod<TInputImage, TOutputImage>(collector,
295+
description + "-Scanline",
296+
CopyScanlineIterator<TInputImage, TOutputImage>,
297+
inputImage.GetPointer(),
298+
outputImage,
299+
iterations);
300+
301+
// Test Method 2: ImageRegionRange
302+
TimeMethod<TInputImage, TOutputImage>(collector,
303+
description + "-Range",
304+
CopyImageRegionRange<TInputImage, TOutputImage>,
305+
inputImage.GetPointer(),
306+
outputImage,
307+
iterations);
308+
309+
// Test Method 1b: Scanline Iterator with NumericTraits
310+
TimeMethod<TInputImage, TOutputImage>(collector,
311+
description + "-Scanline NT",
312+
CopyScanlineIteratorNumericTraits<TInputImage, TOutputImage>,
313+
inputImage.GetPointer(),
314+
outputImage,
315+
iterations);
316+
317+
// Test Method 2b: ImageRegionRange with NumericTraits
318+
TimeMethod<TInputImage, TOutputImage>(collector,
319+
description + "-Range NT",
320+
CopyImageRegionRangeNumericTraits<TInputImage, TOutputImage>,
321+
inputImage.GetPointer(),
322+
outputImage,
323+
iterations);
324+
325+
// Test Method 2c: ImageRegionRange with NumericTraits - buggy version
326+
TimeMethod<TInputImage, TOutputImage>(collector,
327+
description + "-Range NT AsRange",
328+
CopyImageRegionRangeNumericTraitsAsRange<TInputImage, TOutputImage>,
329+
inputImage.GetPointer(),
330+
outputImage,
331+
iterations);
332+
333+
}
334+
335+
336+
int
337+
main(int argc, char * argv[])
338+
{
339+
if (argc < 4)
340+
{
341+
std::cerr << "Usage: " << std::endl;
342+
std::cerr << argv[0] << " timingsFile iterations imageSize" << std::endl;
343+
return EXIT_FAILURE;
344+
}
345+
const std::string timingsFileName = ReplaceOccurrence(argv[1], "__DATESTAMP__", PerfDateStamp());
346+
const int iterations = std::stoi(argv[2]);
347+
const int imageSize = std::stoi(argv[3]);
348+
349+
constexpr unsigned int Dimension = 3;
350+
const auto size = itk::Size<Dimension>::Filled(imageSize);
351+
352+
std::ostringstream oss;
353+
oss << "Image Size: " << size;
354+
std::string description = oss.str();
355+
356+
itk::HighPriorityRealTimeProbesCollector collector;
357+
358+
// Test 1: Image<Vector> to Image<RGBPixel>
359+
TimeIterationMethods<itk::Image<itk::Vector<float, 3>, Dimension>, itk::Image<itk::RGBPixel<double>, Dimension>>(
360+
collector, size, "IVf3->IRGB", iterations);
361+
362+
// Test 2: VectorImage to Image<Vector>
363+
TimeIterationMethods<itk::VectorImage<float, Dimension>, itk::Image<itk::Vector<double, 3>, Dimension>>(
364+
collector, size, "VIf->IVd3", iterations);
365+
366+
// Test 3: Image<Vector> to VectorImage
367+
TimeIterationMethods<itk::Image<itk::Vector<float, 3>, Dimension>, itk::VectorImage<double, Dimension>>(
368+
collector, size, "IVf3->VId", iterations);
369+
370+
// Test 4: VectorImage to VectorImage
371+
TimeIterationMethods<itk::VectorImage<float, Dimension>, itk::VectorImage<double, Dimension>>(
372+
collector, size, "VIf->VId", iterations);
373+
374+
WriteExpandedReport(timingsFileName, collector, true, true, false);
375+
376+
377+
return EXIT_SUCCESS;
378+
}

0 commit comments

Comments
 (0)