Skip to content

Commit

Permalink
Merge branch 'feature/optimise' into develop
Browse files Browse the repository at this point in the history
This PR improves performance of `FlashString` library object iteration and indexing.

For example, calling `indexOf` on an `Array<uint32_t>` object results in repeated calls to the `data()` method.
Further, the C++ iterator implementation does the same.

A new speed test module has been added to evaulate performance. This uses some moderately large test objects and evaluates how long it takes to iterate through them using three methods:

1. regular `for(unsigned i=0; i<array.count(); ++i)` loop
2. C++ iterator `for(auto: array)`
3. Using the `indexOf` method

Array<int> has 1000 elements.
Vector<String> has 367 elements, referencing 3208 bytes of string data.
Map<int, String> has 367 elements.

These are the results, in microseconds, on an esp8266:

| Operation                            | Current      | With this PR |
-------------------------------------- | ------------ | ------------ |
| Array<int> for-loop                  | 1603         | 1611         |
| Array<int> iterator                  | 1326         | 403          |
| Array<int>.indexOf                   | 1303         | 425          |
| Vector<String> for-loop              | 800          | 721          |
| Vector<String> iterator              | 774          | 402          |
| Vector<String> indexOf(const char*)  | 6634         | 969          |
| Vector<String> indexOf(String)       | 849          | 476          |
| Map<int, String> for-loop            | 1042         | 1085         |
| Map<int, String> iterator            | 891          | 577          |
| Map<int, String> indexOf             | 189          | 189          |
| Map<int, String> lookup              | 168          | 168          |

**Optimise iteration and `indexOf` operation**

The regular `[]` operator and `valueAt` methods include range checks which are costly inside a loop.
Adding `unsafe` methods skips these checks.

**Improve Vector search for char* argument**

The `indexOf` method has been specialised for `char*` arguments so that `strlen()` only needs to be called once. Previously it was called on every loop iteration.

**Provide base `==` operator which does binary comparison**

This allows easy searching by value (object content).

**Rationalise string comparison methods**

FlashStrings have three `equals` overloads for `char*`, `String` and `FlashString` arguments.
We also require three `equalsignorecase` equivalents and appropriate `operator==` implementations.

By adding an `ignoreCase` parameter to all three `equals` methods we can simplify code.

**Other improvements**

- Use const references instead of copy
- Move copy/invalidate operations to constructors
- Use constexpr where possible for method return values
- Move `printElement(Print, char)` specialization into source file
- Move `read` method out of header
- Add github workflow
- Put strings in flash
  • Loading branch information
mikee47 committed Apr 10, 2024
2 parents 4821edc + a6a74a2 commit d7a129d
Show file tree
Hide file tree
Showing 22 changed files with 897 additions and 199 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/ci-dispatch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CI Dispatch

on:
workflow_dispatch:
inputs:
sming_repo:
description: 'Full URL for Sming repository'
default: 'https://github.com/SmingHub/Sming'
type: string
sming_branch:
description: 'Sming branch to run against'
default: 'develop'
type: string

jobs:
build:
uses: SmingHub/Sming/.github/workflows/library.yml@develop
with:
sming_repo: ${{ inputs.sming_repo }}
sming_branch: ${{ inputs.sming_branch }}
7 changes: 7 additions & 0 deletions .github/workflows/ci-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: CI Push

on: [push, pull_request]

jobs:
build:
uses: SmingHub/Sming/.github/workflows/library.yml@develop
76 changes: 76 additions & 0 deletions src/ArrayPrinter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/****
* ArrayPrinter.cpp - Print support for arrays
*
* Copyright 2019 mikee47 <mike@sillyhouse.net>
*
* This file is part of the FlashString Library
*
* This library is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, version 3 or later.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this library.
* If not, see <https://www.gnu.org/licenses/>.
*
****/

#include "include/FlashString/ArrayPrinter.hpp"

namespace FSTR
{
size_t printElement(Print& p, char c)
{
auto escape = [](char c) -> char {
switch(c) {
case '\0':
return '0';
case '\'':
return '\'';
case '\"':
return '"';
case '\?':
return '?';
case '\\':
return '\\';
case '\a':
return 'a';
case '\b':
return 'b';
case '\f':
return 'f';
case '\n':
return 'n';
case '\r':
return 'r';
case '\t':
return 't';
case '\v':
return 'v';
default:
return '\0';
}
};

char buf[8];
char* o = buf;
*o++ = '\'';
char esc = escape(c);
if(esc) {
*o++ = '\\';
*o++ = esc;
} else if(isprint(c)) {
*o++ = c;
} else {
*o++ = '\\';
*o++ = 'x';
*o++ = hexchar(uint8_t(c) >> 4);
*o++ = hexchar(uint8_t(c) & 0x0f);
}
*o++ = '\'';
return p.write(buf, o - buf);
}

} // namespace FSTR
36 changes: 18 additions & 18 deletions src/ObjectBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ namespace FSTR
const ObjectBase ObjectBase::empty_{ObjectBase::lengthInvalid};
constexpr uint32_t ObjectBase::copyBit;

bool ObjectBase::operator==(const ObjectBase& other) const
{
if(this == &other) {
return true;
}
if(length() != other.length()) {
return false;
}
return memcmp(this, &other, sizeof(flashLength_) + size()) == 0;
}

size_t ObjectBase::readFlash(size_t offset, void* buffer, size_t count) const
{
auto len = length();
Expand All @@ -37,15 +48,16 @@ size_t ObjectBase::readFlash(size_t offset, void* buffer, size_t count) const
return flashmem_read(buffer, addr, count);
}

size_t ObjectBase::length() const
size_t ObjectBase::read(size_t offset, void* buffer, size_t count) const
{
if(isNull()) {
auto len = length();
if(offset >= len) {
return 0;
} else if(isCopy()) {
return reinterpret_cast<const ObjectBase*>(flashLength_ & ~copyBit)->length();
} else {
return flashLength_;
}

count = std::min(len - offset, count);
memcpy_P(buffer, data() + offset, count);
return count;
}

const uint8_t* ObjectBase::data() const
Expand All @@ -71,16 +83,4 @@ const uint8_t* ObjectBase::data() const
return reinterpret_cast<const uint8_t*>(&ptr->flashLength_ + 1);
}

void ObjectBase::invalidate()
{
#ifndef ARCH_HOST
// Illegal on real flash object
assert(!isFlashPtr(this));
if(isFlashPtr(this)) {
return;
}
#endif
flashLength_ = lengthInvalid;
}

} // namespace FSTR
47 changes: 26 additions & 21 deletions src/String.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,44 @@

namespace FSTR
{
bool String::equals(const char* cstr, size_t len) const
bool String::equals(const char* cstr, size_t clen, bool ignoreCase) const
{
// Unlikely we'd want an empty flash string, but check anyway
if(cstr == nullptr) {
return length() == 0;
}
// Don't use strcmp as our data may contain nuls
if(len == 0) {
len = strlen(cstr);
}
if(len != length()) {
auto len = length();
if(clen != len) {
return false;
}
LOAD_FSTR(buf, *this);
if(ignoreCase) {
return memicmp(buf, cstr, len) == 0;
}
return memcmp(buf, cstr, len) == 0;
}

bool String::equals(const String& str) const
bool String::equals(const char* cstr, bool ignoreCase) const
{
return equals(cstr, cstr ? strlen(cstr) : 0, ignoreCase);
}

bool String::equals(const String& str, bool ignoreCase) const
{
if(data() == str.data()) {
auto dataptr = data();
auto strdata = str.data();
if(dataptr == strdata) {
return true;
}
if(length() != str.length()) {
auto len = length();
if(len != str.length()) {
return false;
}
return memcmp_aligned(data(), str.data(), length()) == 0;
if(ignoreCase) {
LOAD_FSTR(buf, *this);
return memicmp(dataptr, buf, len) == 0;
}
return memcmp_aligned(dataptr, strdata, len) == 0;
}

/* Wiring String support */
Expand All @@ -58,25 +70,18 @@ String::operator WString() const
return isNull() ? WString() : WString(data(), length());
}

bool String::equals(const WString& str) const
bool String::equals(const WString& str, bool ignoreCase) const
{
auto len = str.length();
if(len != length()) {
return false;
}
// @todo optimise memcmp_P then we won't need to load entire String into RAM first
LOAD_FSTR(buf, *this);
return memcmp(buf, str.c_str(), len) == 0;
}

bool String::equalsIgnoreCase(const WString& str) const
{
auto len = str.length();
if(len != length()) {
return false;
if(ignoreCase) {
return memicmp(buf, str.c_str(), len) == 0;
}
LOAD_FSTR(buf, *this);
return memicmp(buf, str.c_str(), len) == 0;
return memcmp(buf, str.c_str(), len) == 0;
}

} // namespace FSTR
2 changes: 2 additions & 0 deletions src/include/FlashString/Array.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ namespace FSTR
template <typename ElementType> class Array : public Object<Array<ElementType>, ElementType>
{
public:
static_assert(!std::is_pointer<ElementType>::value, "Pointer types not supported by Array - use Vector");

/* Arduino Print support */

/**
Expand Down
57 changes: 4 additions & 53 deletions src/include/FlashString/ArrayPrinter.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****
* ArrayPrinter.cpp - Print support for arrays
* ArrayPrinter.hpp - Print support for arrays
*
* Copyright 2019 mikee47 <mike@sillyhouse.net>
*
Expand All @@ -24,62 +24,13 @@

namespace FSTR
{
template <typename T> typename std::enable_if<!std::is_same<T, char>::value, size_t>::type printElement(Print& p, T e)
template <typename T>
typename std::enable_if<!std::is_same<T, char>::value, size_t>::type printElement(Print& p, const T& e)
{
return print(p, e);
}

template <typename T> typename std::enable_if<std::is_same<T, char>::value, size_t>::type printElement(Print& p, T c)
{
auto escape = [](char c) -> char {
switch(c) {
case '\0':
return '0';
case '\'':
return '\'';
case '\"':
return '"';
case '\?':
return '?';
case '\\':
return '\\';
case '\a':
return 'a';
case '\b':
return 'b';
case '\f':
return 'f';
case '\n':
return 'n';
case '\r':
return 'r';
case '\t':
return 't';
case '\v':
return 'v';
default:
return '\0';
}
};

char buf[8];
char* o = buf;
*o++ = '\'';
char esc = escape(c);
if(esc) {
*o++ = '\\';
*o++ = esc;
} else if(isprint(c)) {
*o++ = c;
} else {
*o++ = '\\';
*o++ = 'x';
*o++ = hexchar(uint8_t(c) >> 4);
*o++ = hexchar(uint8_t(c) & 0x0f);
}
*o++ = '\'';
return p.write(buf, o - buf);
}
size_t printElement(Print& p, char c);

/**
* @brief Class template to provide a simple way to print the contents of an array
Expand Down
4 changes: 2 additions & 2 deletions src/include/FlashString/MapPair.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ template <typename KeyType, class ContentType> class MapPair

if(*this) {
count += print(p, key());
count += p.print(" => ");
count += p.print(_F(" => "));
count += print(p, content());
} else {
count += p.print("(invalid)");
count += p.print(_F("(invalid)"));
}

return count;
Expand Down

0 comments on commit d7a129d

Please sign in to comment.