blob: a1c9e7c6b66d0ea9cf4b309d37a46f9b604d120e [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/platform/audio/vector_math.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <numeric>
#include <random>
#include <vector>
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
namespace blink {
namespace vector_math {
namespace {
struct MemoryLayout {
size_t byte_alignment;
size_t stride;
};
// This is the minimum aligned needed by AVX on x86 family architectures.
constexpr size_t kMaxBitAlignment = 256u;
constexpr size_t kMaxByteAlignment = kMaxBitAlignment / 8u;
constexpr size_t kMaxStride = 2u;
constexpr MemoryLayout kMemoryLayouts[] = {
{kMaxByteAlignment / 4u, 1u},
{kMaxByteAlignment / 2u, 1u},
{kMaxByteAlignment / 2u + kMaxByteAlignment / 4u, 1u},
{kMaxByteAlignment, 1u},
{0u, kMaxStride}};
constexpr size_t kMemoryLayoutCount =
sizeof(kMemoryLayouts) / sizeof(*kMemoryLayouts);
// This is the minimum vector size in bytes needed for MSA instructions on
// MIPS.
constexpr size_t kMaxVectorSizeInBytes = 1024u;
constexpr size_t kVectorSizesInBytes[] = {
kMaxVectorSizeInBytes,
// This vector size in bytes is chosen so that the following optimization
// paths can be tested on x86 family architectures using different memory
// layouts:
// * AVX + SSE + scalar
// * scalar + SSE + AVX
// * SSE + AVX + scalar
// * scalar + AVX + SSE
// On other architectures, this vector size in bytes results in either
// optimization + scalar path or scalar path to be tested.
kMaxByteAlignment + kMaxByteAlignment / 2u + kMaxByteAlignment / 4u};
constexpr size_t kVectorSizeCount =
sizeof(kVectorSizesInBytes) / sizeof(*kVectorSizesInBytes);
// Compare two floats and consider all NaNs to be equal.
bool Equal(float a, float b) {
if (std::isnan(a))
return std::isnan(b);
return a == b;
}
// This represents a real source or destination vector which is aligned, can be
// non-contiguous and can be used as a source or destination vector for
// blink::vector_math functions.
template <typename T>
class TestVector {
class Iterator {
public:
// These types are used by std::iterator_traits used by std::equal used by
// TestVector::operator==.
using difference_type = ptrdiff_t;
using iterator_category = std::bidirectional_iterator_tag;
using pointer = T*;
using reference = T&;
using value_type = T;
Iterator(T* p, int stride) : p_(p), stride_(stride) {}
Iterator& operator++() {
p_ += stride_;
return *this;
}
Iterator operator++(int) {
Iterator iter = *this;
++(*this);
return iter;
}
Iterator& operator--() {
p_ -= stride_;
return *this;
}
Iterator operator--(int) {
Iterator iter = *this;
--(*this);
return iter;
}
bool operator==(const Iterator& other) const { return p_ == other.p_; }
bool operator!=(const Iterator& other) const { return !(*this == other); }
T& operator*() const { return *p_; }
private:
T* p_;
size_t stride_;
};
public:
using ReverseIterator = std::reverse_iterator<Iterator>;
// These types are used internally by Google Test.
using const_iterator = Iterator;
using iterator = Iterator;
TestVector() = default;
TestVector(T* base, const MemoryLayout* memory_layout, size_t size)
: p_(GetAligned(base, memory_layout->byte_alignment)),
memory_layout_(memory_layout),
size_(size) {}
TestVector(T* base, const TestVector<const T>& primary_vector)
: TestVector(base,
primary_vector.memory_layout(),
primary_vector.size()) {}
Iterator begin() const { return Iterator(p_, stride()); }
Iterator end() const { return Iterator(p_ + size() * stride(), stride()); }
ReverseIterator rbegin() const { return ReverseIterator(end()); }
ReverseIterator rend() const { return ReverseIterator(begin()); }
const MemoryLayout* memory_layout() const { return memory_layout_; }
T* p() const { return p_; }
size_t size() const { return size_; }
int stride() const { return static_cast<int>(memory_layout()->stride); }
bool operator==(const TestVector& other) const {
return std::equal(begin(), end(), other.begin(), other.end(), Equal);
}
T& operator[](size_t i) const { return p_[i * stride()]; }
private:
static T* GetAligned(T* base, size_t byte_alignment) {
size_t base_byte_alignment = GetByteAlignment(base);
size_t byte_offset =
(byte_alignment - base_byte_alignment + kMaxByteAlignment) %
kMaxByteAlignment;
T* p = base + byte_offset / sizeof(T);
size_t p_byte_alignment = GetByteAlignment(p);
CHECK_EQ(byte_alignment % kMaxByteAlignment, p_byte_alignment);
return p;
}
static size_t GetByteAlignment(T* p) {
return reinterpret_cast<size_t>(p) % kMaxByteAlignment;
}
T* p_;
const MemoryLayout* memory_layout_;
size_t size_;
};
// Get primary input vectors with difference memory layout and size
// combinations.
template <typename T>
std::array<TestVector<const T>, kVectorSizeCount * kMemoryLayoutCount>
GetPrimaryVectors(const T* base) {
std::array<TestVector<const T>, kVectorSizeCount * kMemoryLayoutCount>
vectors;
for (auto& vector : vectors) {
ptrdiff_t i = &vector - &vectors[0];
ptrdiff_t memory_layout_index = i % kMemoryLayoutCount;
ptrdiff_t size_index = i / kMemoryLayoutCount;
vector = TestVector<const T>(base, &kMemoryLayouts[memory_layout_index],
kVectorSizesInBytes[size_index] / sizeof(T));
}
return vectors;
}
// Get secondary input or output vectors. As the size of a secondary vector
// must always be the same as the size of the primary input vector, there are
// only two interesting secondary vectors:
// - A vector with the same memory layout as the primary input vector has and
// which therefore is aligned whenever the primary input vector is aligned.
// - A vector with a different memory layout than the primary input vector has
// and which therefore is not aligned when the primary input vector is
// aligned.
template <typename T>
std::array<TestVector<T>, 2u> GetSecondaryVectors(
T* base,
const MemoryLayout* primary_memory_layout,
size_t size) {
std::array<TestVector<T>, 2u> vectors;
const MemoryLayout* other_memory_layout =
&kMemoryLayouts[primary_memory_layout == &kMemoryLayouts[0]];
CHECK_NE(primary_memory_layout, other_memory_layout);
CHECK_NE(primary_memory_layout->byte_alignment,
other_memory_layout->byte_alignment);
vectors[0] = TestVector<T>(base, primary_memory_layout, size);
vectors[1] = TestVector<T>(base, other_memory_layout, size);
return vectors;
}
template <typename T>
std::array<TestVector<T>, 2u> GetSecondaryVectors(
T* base,
const TestVector<const float>& primary_vector) {
return GetSecondaryVectors(base, primary_vector.memory_layout(),
primary_vector.size());
}
class VectorMathTest : public testing::Test {
protected:
enum {
kDestinationCount = 4u,
kFloatArraySize =
(kMaxStride * kMaxVectorSizeInBytes + kMaxByteAlignment - 1u) /
sizeof(float),
kFullyFiniteSource = 4u,
kFullyFiniteSource2 = 5u,
kFullyNonNanSource = 6u,
kSourceCount = 7u
};
// Get a destination buffer containing initially uninitialized floats.
float* GetDestination(size_t i) {
CHECK_LT(i, static_cast<size_t>(kDestinationCount));
return destinations_[i];
}
// Get a source buffer containing random floats.
const float* GetSource(size_t i) {
CHECK_LT(i, static_cast<size_t>(kSourceCount));
return sources_[i];
}
static void SetUpTestCase() {
std::minstd_rand generator(3141592653u);
// Fill in source buffers with finite random floats.
std::uniform_real_distribution<float> float_distribution(-10.0f, 10.0f);
std::generate_n(&**sources_, sizeof(sources_) / sizeof(**sources_),
[&]() { return float_distribution(generator); });
// Add INFINITYs and NANs to most source buffers.
std::uniform_int_distribution<size_t> index_distribution(
0u, kFloatArraySize / 2u - 1u);
for (size_t i = 0u; i < kSourceCount; ++i) {
if (i == kFullyFiniteSource || i == kFullyFiniteSource2)
continue;
sources_[i][index_distribution(generator)] = INFINITY;
sources_[i][index_distribution(generator)] = -INFINITY;
if (i != kFullyNonNanSource)
sources_[i][index_distribution(generator)] = NAN;
}
}
private:
static float destinations_[kDestinationCount][kFloatArraySize];
static float sources_[kSourceCount][kFloatArraySize];
};
float VectorMathTest::destinations_[kDestinationCount][kFloatArraySize];
float VectorMathTest::sources_[kSourceCount][kFloatArraySize];
TEST_F(VectorMathTest, Conv) {
for (const auto& source : GetPrimaryVectors(GetSource(kFullyFiniteSource))) {
if (source.stride() != 1)
continue;
for (size_t filter_size : {3u, 32u, 64u, 128u}) {
// The maximum number of frames which could be processed here is
// |source.size() - filter_size + 1|. However, in order to test
// optimization paths, |frames_to_process| should be optimal (divisible
// by a power of 2) whenever |filter_size| is optimal. Therefore, let's
// process only |source.size() - filter_size| frames here.
if (filter_size >= source.size())
break;
size_t frames_to_process = source.size() - filter_size;
// The stride of a convolution filter must be -1. Let's first create
// a reversed filter whose stride is 1.
TestVector<const float> reversed_filter(
GetSource(kFullyFiniteSource2), source.memory_layout(), filter_size);
// The filter begins from the reverse beginning of the reversed filter
// and grows downwards.
const float* filter_p = &*reversed_filter.rbegin();
TestVector<float> expected_dest(
GetDestination(0u), source.memory_layout(), frames_to_process);
for (size_t i = 0u; i < frames_to_process; ++i) {
expected_dest[i] = 0u;
for (size_t j = 0u; j < filter_size; ++j)
expected_dest[i] += source[i + j] * *(filter_p - j);
}
for (auto& dest : GetSecondaryVectors(
GetDestination(1u), source.memory_layout(), frames_to_process)) {
AudioFloatArray prepared_filter;
PrepareFilterForConv(filter_p, -1, filter_size, &prepared_filter);
Conv(source.p(), 1, filter_p, -1, dest.p(), 1, frames_to_process,
filter_size, &prepared_filter);
for (size_t i = 0u; i < frames_to_process; ++i) {
EXPECT_NEAR(expected_dest[i], dest[i],
1e-3 * std::abs(expected_dest[i]));
}
}
}
}
}
TEST_F(VectorMathTest, Vadd) {
for (const auto& source1 : GetPrimaryVectors(GetSource(0u))) {
for (const auto& source2 : GetSecondaryVectors(GetSource(1u), source1)) {
TestVector<float> expected_dest(GetDestination(0u), source1);
for (size_t i = 0u; i < source1.size(); ++i)
expected_dest[i] = source1[i] + source2[i];
for (auto& dest : GetSecondaryVectors(GetDestination(1u), source1)) {
Vadd(source1.p(), source1.stride(), source2.p(), source2.stride(),
dest.p(), dest.stride(), source1.size());
EXPECT_EQ(expected_dest, dest);
}
}
}
}
TEST_F(VectorMathTest, Vclip) {
// Vclip does not accept NaNs thus let's use only sources without NaNs.
for (const auto& source : GetPrimaryVectors(GetSource(kFullyNonNanSource))) {
const float* thresholds = GetSource(kFullyFiniteSource);
const float low_threshold = std::min(thresholds[0], thresholds[1]);
const float high_threshold = std::max(thresholds[0], thresholds[1]);
TestVector<float> expected_dest(GetDestination(0u), source);
for (size_t i = 0u; i < source.size(); ++i)
expected_dest[i] = clampTo(source[i], low_threshold, high_threshold);
for (auto& dest : GetSecondaryVectors(GetDestination(1u), source)) {
Vclip(source.p(), source.stride(), &low_threshold, &high_threshold,
dest.p(), dest.stride(), source.size());
EXPECT_EQ(expected_dest, dest);
}
}
}
TEST_F(VectorMathTest, Vmaxmgv) {
const auto maxmg = [](float init, float x) {
return std::max(init, std::abs(x));
};
// Vmaxmgv does not accept NaNs thus let's use only sources without NaNs.
for (const float* source_base :
{GetSource(kFullyFiniteSource), GetSource(kFullyNonNanSource)}) {
for (const auto& source : GetPrimaryVectors(source_base)) {
const float expected_max =
std::accumulate(source.begin(), source.end(), 0.0f, maxmg);
float max;
Vmaxmgv(source.p(), source.stride(), &max, source.size());
EXPECT_EQ(expected_max, max) << testing::PrintToString(source);
}
}
}
TEST_F(VectorMathTest, Vmul) {
for (const auto& source1 : GetPrimaryVectors(GetSource(0u))) {
for (const auto& source2 : GetSecondaryVectors(GetSource(1u), source1)) {
TestVector<float> expected_dest(GetDestination(0u), source1);
for (size_t i = 0u; i < source1.size(); ++i)
expected_dest[i] = source1[i] * source2[i];
for (auto& dest : GetSecondaryVectors(GetDestination(1u), source1)) {
Vmul(source1.p(), source1.stride(), source2.p(), source2.stride(),
dest.p(), dest.stride(), source1.size());
EXPECT_EQ(expected_dest, dest);
}
}
}
}
TEST_F(VectorMathTest, Vsma) {
for (const auto& source : GetPrimaryVectors(GetSource(0u))) {
const float scale = *GetSource(1u);
const TestVector<const float> dest_source(GetSource(2u), source);
TestVector<float> expected_dest(GetDestination(0u), source);
for (size_t i = 0u; i < source.size(); ++i)
expected_dest[i] = dest_source[i] + scale * source[i];
for (auto& dest : GetSecondaryVectors(GetDestination(1u), source)) {
std::copy(dest_source.begin(), dest_source.end(), dest.begin());
Vsma(source.p(), source.stride(), &scale, dest.p(), dest.stride(),
source.size());
// Different optimizations may use different precisions for intermediate
// results which may result in different rounding errors thus let's
// expect only mostly equal floats.
for (size_t i = 0u; i < source.size(); ++i) {
if (std::isfinite(expected_dest[i])) {
#if defined(OS_MACOSX)
// On Mac, OS provided vectorized functions are used which may result
// in bigger rounding errors than functions used on other OSes.
EXPECT_NEAR(expected_dest[i], dest[i],
1e-5 * std::abs(expected_dest[i]));
#else
EXPECT_FLOAT_EQ(expected_dest[i], dest[i]);
#endif
} else {
EXPECT_PRED2(Equal, expected_dest[i], dest[i]);
}
}
}
}
}
TEST_F(VectorMathTest, Vsmul) {
for (const auto& source : GetPrimaryVectors(GetSource(0u))) {
const float scale = *GetSource(1u);
TestVector<float> expected_dest(GetDestination(0u), source);
for (size_t i = 0u; i < source.size(); ++i)
expected_dest[i] = scale * source[i];
for (auto& dest : GetSecondaryVectors(GetDestination(1u), source)) {
Vsmul(source.p(), source.stride(), &scale, dest.p(), dest.stride(),
source.size());
EXPECT_EQ(expected_dest, dest);
}
}
}
TEST_F(VectorMathTest, Vsvesq) {
const auto sqsum = [](float init, float x) { return init + x * x; };
for (const float* source_base :
{GetSource(0u), GetSource(kFullyFiniteSource)}) {
for (const auto& source : GetPrimaryVectors(source_base)) {
const float expected_sum =
std::accumulate(source.begin(), source.end(), 0.0f, sqsum);
float sum;
Vsvesq(source.p(), source.stride(), &sum, source.size());
if (std::isfinite(expected_sum)) {
// Optimized paths in Vsvesq use parallel partial sums which may result
// in different rounding errors than the non-partial sum algorithm used
// here and in non-optimized paths in Vsvesq.
EXPECT_FLOAT_EQ(expected_sum, sum);
} else {
EXPECT_PRED2(Equal, expected_sum, sum);
}
}
}
}
TEST_F(VectorMathTest, Zvmul) {
constexpr float kMax = std::numeric_limits<float>::max();
std::vector<std::array<float, kFloatArraySize + 1u>> sources(4u);
for (size_t i = 0u; i < sources.size(); ++i) {
// Initialize a local source with a randomized test case source.
std::copy_n(GetSource(i), kFloatArraySize, sources[i].begin());
// Put +FLT_MAX and -FLT_MAX in the middle of the source. Use a different
// sequence for each source in order to get 16 different combinations.
for (size_t j = 0u; j < 16u; ++j)
sources[i][kFloatArraySize / 2u + j] = ((j >> i) & 1) ? -kMax : kMax;
}
for (const auto& real1 : GetPrimaryVectors(sources[0u].data())) {
if (real1.stride() != 1)
continue;
const TestVector<const float> imag1(sources[1u].data(), real1);
const TestVector<const float> real2(sources[2u].data(), real1);
const TestVector<const float> imag2(sources[3u].data(), real1);
TestVector<float> expected_dest_real(GetDestination(0u), real1);
TestVector<float> expected_dest_imag(GetDestination(1u), real1);
for (size_t i = 0u; i < real1.size(); ++i) {
expected_dest_real[i] = real1[i] * real2[i] - imag1[i] * imag2[i];
expected_dest_imag[i] = real1[i] * imag2[i] + imag1[i] * real2[i];
if (&real1[i] >= &sources[0u][kFloatArraySize / 2u] &&
&real1[i] < &sources[0u][kFloatArraySize / 2u] + 16u) {
// FLT_MAX products should have overflowed.
EXPECT_TRUE(std::isinf(expected_dest_real[i]) ||
std::isinf(expected_dest_imag[i]));
EXPECT_TRUE(std::isnan(expected_dest_real[i]) ||
std::isnan(expected_dest_imag[i]));
}
}
for (auto& dest_real : GetSecondaryVectors(GetDestination(2u), real1)) {
TestVector<float> dest_imag(GetDestination(3u), real1);
ASSERT_EQ(1, dest_real.stride());
Zvmul(real1.p(), imag1.p(), real2.p(), imag2.p(), dest_real.p(),
dest_imag.p(), real1.size());
// Different optimizations may use different precisions for intermediate
// results which may result in different rounding errors thus let's
// expect only mostly equal floats.
for (size_t i = 0u; i < real1.size(); ++i) {
if (std::isfinite(expected_dest_real[i])) {
#if defined(OS_MACOSX)
// On Mac, OS provided vectorized functions are used which may result
// in bigger rounding errors than functions used on other OSes.
EXPECT_NEAR(expected_dest_real[i], dest_real[i],
1e-5 * std::abs(expected_dest_real[i]));
#else
EXPECT_FLOAT_EQ(expected_dest_real[i], dest_real[i]);
#endif
} else {
#if defined(OS_MACOSX)
// On Mac, OS provided vectorized functions are used which may result
// in different NaN handling than functions used on other OSes.
EXPECT_TRUE(!std::isfinite(dest_real[i]));
#else
EXPECT_PRED2(Equal, expected_dest_real[i], dest_real[i]);
#endif
}
if (std::isfinite(expected_dest_imag[i])) {
#if defined(OS_MACOSX)
// On Mac, OS provided vectorized functions are used which may result
// in bigger rounding errors than functions used on other OSes.
EXPECT_NEAR(expected_dest_imag[i], dest_imag[i],
1e-5 * std::abs(expected_dest_imag[i]));
#else
EXPECT_FLOAT_EQ(expected_dest_imag[i], dest_imag[i]);
#endif
} else {
#if defined(OS_MACOSX)
// On Mac, OS provided vectorized functions are used which may result
// in different NaN handling than functions used on other OSes.
EXPECT_TRUE(!std::isfinite(dest_imag[i]));
#else
EXPECT_PRED2(Equal, expected_dest_imag[i], dest_imag[i]);
#endif
}
}
}
}
}
} // namespace
} // namespace vector_math
} // namespace blink