[flang][runtime] Formatted input optimizations (#134715)

Make some minor tweaks (inlining, caching) to the formatting input path
to improve integer input in a SPEC code. (None of the I/O library has
been tuned yet for performance, and there are some easy optimizations
for common cases.) Input integer values are now calculated with native
C/C++ 128-bit integers.

A benchmark that only reads about 5M lines of three integer values each
speeds up from over 8 seconds to under 3 in my environment with these
changeds.

If this works out, the code here can be used to optimize the formatted
input paths for real and character data, too.

Fixes https://github.com/llvm/llvm-project/issues/134026.
This commit is contained in:
Peter Klausler
2025-04-10 09:56:46 -07:00
committed by GitHub
parent 750d009bb2
commit 18fe0124e7
8 changed files with 219 additions and 94 deletions

View File

@@ -45,19 +45,36 @@ struct ConnectionAttributes {
};
struct ConnectionState : public ConnectionAttributes {
RT_API_ATTRS bool
IsAtEOF() const; // true when read has hit EOF or endfile record
RT_API_ATTRS bool
IsAfterEndfile() const; // true after ENDFILE until repositioned
RT_API_ATTRS bool IsAtEOF() const {
// true when read has hit EOF or endfile record
return endfileRecordNumber && currentRecordNumber >= *endfileRecordNumber;
}
RT_API_ATTRS bool IsAfterEndfile() const {
// true after ENDFILE until repositioned
return endfileRecordNumber && currentRecordNumber > *endfileRecordNumber;
}
// All positions and measurements are always in units of bytes,
// not characters. Multi-byte character encodings are possible in
// both internal I/O (when the character kind of the variable is 2 or 4)
// and external formatted I/O (when the encoding is UTF-8).
RT_API_ATTRS std::size_t RemainingSpaceInRecord() const;
RT_API_ATTRS bool NeedAdvance(std::size_t) const;
RT_API_ATTRS void HandleAbsolutePosition(std::int64_t);
RT_API_ATTRS void HandleRelativePosition(std::int64_t);
RT_API_ATTRS std::size_t RemainingSpaceInRecord() const {
auto recl{recordLength.value_or(openRecl.value_or(
executionEnvironment.listDirectedOutputLineLengthLimit))};
return positionInRecord >= recl ? 0 : recl - positionInRecord;
}
RT_API_ATTRS bool NeedAdvance(std::size_t width) const {
return positionInRecord > 0 && width > RemainingSpaceInRecord();
}
RT_API_ATTRS void HandleAbsolutePosition(std::int64_t n) {
positionInRecord = (n < 0 ? 0 : n) + leftTabLimit.value_or(0);
}
RT_API_ATTRS void HandleRelativePosition(std::int64_t n) {
auto least{leftTabLimit.value_or(0)};
auto newPos{positionInRecord + n};
positionInRecord = newPos < least ? least : newPos;
;
}
RT_API_ATTRS void BeginRecord() {
positionInRecord = 0;

View File

@@ -130,20 +130,94 @@ public:
}
// Vacant after the end of the current record
RT_API_ATTRS Fortran::common::optional<char32_t> GetCurrentChar(
RT_API_ATTRS Fortran::common::optional<char32_t> GetCurrentCharSlow(
std::size_t &byteCount);
// For faster formatted input editing, this structure can be built by
// GetUpcomingFastAsciiField() and used to save significant time in
// GetCurrentChar, NextInField() and other input utilities when the input
// is buffered, does not require UTF-8 conversion, and comprises only
// single byte characters.
class FastAsciiField {
public:
RT_API_ATTRS FastAsciiField(ConnectionState &connection)
: connection_{connection} {}
RT_API_ATTRS FastAsciiField(
ConnectionState &connection, const char *start, std::size_t bytes)
: connection_{connection}, at_{start}, limit_{start + bytes} {
CheckForAsterisk();
}
RT_API_ATTRS ConnectionState &connection() { return connection_; }
RT_API_ATTRS std::size_t got() const { return got_; }
RT_API_ATTRS bool MustUseSlowPath() const { return at_ == nullptr; }
RT_API_ATTRS Fortran::common::optional<char32_t> Next() const {
if (at_ && at_ < limit_) {
return *at_;
} else {
return std::nullopt;
}
}
RT_API_ATTRS void NextRecord(IoStatementState &io) {
if (at_) {
if (std::size_t bytes{io.GetNextInputBytes(at_)}) {
limit_ = at_ + bytes;
CheckForAsterisk();
} else {
at_ = limit_ = nullptr;
}
}
}
RT_API_ATTRS void Advance(int gotten, std::size_t bytes) {
if (at_ && at_ < limit_) {
++at_;
got_ += gotten;
}
connection_.HandleRelativePosition(bytes);
}
RT_API_ATTRS bool MightHaveAsterisk() const { return !at_ || hasAsterisk_; }
private:
RT_API_ATTRS void CheckForAsterisk() {
hasAsterisk_ =
at_ && at_ < limit_ && std::memchr(at_, '*', limit_ - at_) != nullptr;
}
ConnectionState &connection_;
const char *at_{nullptr};
const char *limit_{nullptr};
std::size_t got_{0}; // for READ(..., SIZE=)
bool hasAsterisk_{false};
};
RT_API_ATTRS FastAsciiField GetUpcomingFastAsciiField();
RT_API_ATTRS Fortran::common::optional<char32_t> GetCurrentChar(
std::size_t &byteCount, FastAsciiField *field = nullptr) {
if (field) {
if (auto ch{field->Next()}) {
byteCount = ch ? 1 : 0;
return ch;
} else if (!field->MustUseSlowPath()) {
return std::nullopt;
}
}
return GetCurrentCharSlow(byteCount);
}
// The result of CueUpInput() and the "remaining" arguments to SkipSpaces()
// and NextInField() are always in units of bytes, not characters; the
// distinction matters for internal input from CHARACTER(KIND=2 and 4).
// For fixed-width fields, return the number of remaining bytes.
// Skip over leading blanks.
RT_API_ATTRS Fortran::common::optional<int> CueUpInput(const DataEdit &edit) {
RT_API_ATTRS Fortran::common::optional<int> CueUpInput(
const DataEdit &edit, FastAsciiField *fastField = nullptr) {
Fortran::common::optional<int> remaining;
if (edit.IsListDirected()) {
std::size_t byteCount{0};
GetNextNonBlank(byteCount);
GetNextNonBlank(byteCount, fastField);
} else {
if (edit.width.value_or(0) > 0) {
remaining = *edit.width;
@@ -152,16 +226,17 @@ public:
*remaining *= bytesPerChar;
}
}
SkipSpaces(remaining);
SkipSpaces(remaining, fastField);
}
return remaining;
}
RT_API_ATTRS Fortran::common::optional<char32_t> SkipSpaces(
Fortran::common::optional<int> &remaining) {
Fortran::common::optional<int> &remaining,
FastAsciiField *fastField = nullptr) {
while (!remaining || *remaining > 0) {
std::size_t byteCount{0};
if (auto ch{GetCurrentChar(byteCount)}) {
if (auto ch{GetCurrentChar(byteCount, fastField)}) {
if (*ch != ' ' && *ch != '\t') {
return ch;
}
@@ -172,7 +247,11 @@ public:
GotChar(byteCount);
*remaining -= byteCount;
}
HandleRelativePosition(byteCount);
if (fastField) {
fastField->Advance(0, byteCount);
} else {
HandleRelativePosition(byteCount);
}
} else {
break;
}
@@ -183,25 +262,35 @@ public:
// Acquires the next input character, respecting any applicable field width
// or separator character.
RT_API_ATTRS Fortran::common::optional<char32_t> NextInField(
Fortran::common::optional<int> &remaining, const DataEdit &);
Fortran::common::optional<int> &remaining, const DataEdit &,
FastAsciiField *field = nullptr);
// Detect and signal any end-of-record condition after input.
// Returns true if at EOR and remaining input should be padded with blanks.
RT_API_ATTRS bool CheckForEndOfRecord(std::size_t afterReading);
RT_API_ATTRS bool CheckForEndOfRecord(
std::size_t afterReading, const ConnectionState &);
// Skips spaces, advances records, and ignores NAMELIST comments
RT_API_ATTRS Fortran::common::optional<char32_t> GetNextNonBlank(
std::size_t &byteCount) {
auto ch{GetCurrentChar(byteCount)};
std::size_t &byteCount, FastAsciiField *fastField = nullptr) {
auto ch{GetCurrentChar(byteCount, fastField)};
bool inNamelist{mutableModes().inNamelist};
while (!ch || *ch == ' ' || *ch == '\t' || *ch == '\n' ||
(inNamelist && *ch == '!')) {
if (ch && (*ch == ' ' || *ch == '\t' || *ch == '\n')) {
HandleRelativePosition(byteCount);
} else if (!AdvanceRecord()) {
if (fastField) {
fastField->Advance(0, byteCount);
} else {
HandleRelativePosition(byteCount);
}
} else if (AdvanceRecord()) {
if (fastField) {
fastField->NextRecord(*this);
}
} else {
return Fortran::common::nullopt;
}
ch = GetCurrentChar(byteCount);
ch = GetCurrentChar(byteCount, fastField);
}
return ch;
}

View File

@@ -9,37 +9,10 @@
#include "flang-rt/runtime/connection.h"
#include "flang-rt/runtime/environment.h"
#include "flang-rt/runtime/io-stmt.h"
#include <algorithm>
namespace Fortran::runtime::io {
RT_OFFLOAD_API_GROUP_BEGIN
RT_API_ATTRS std::size_t ConnectionState::RemainingSpaceInRecord() const {
auto recl{recordLength.value_or(openRecl.value_or(
executionEnvironment.listDirectedOutputLineLengthLimit))};
return positionInRecord >= recl ? 0 : recl - positionInRecord;
}
RT_API_ATTRS bool ConnectionState::NeedAdvance(std::size_t width) const {
return positionInRecord > 0 && width > RemainingSpaceInRecord();
}
RT_API_ATTRS bool ConnectionState::IsAtEOF() const {
return endfileRecordNumber && currentRecordNumber >= *endfileRecordNumber;
}
RT_API_ATTRS bool ConnectionState::IsAfterEndfile() const {
return endfileRecordNumber && currentRecordNumber > *endfileRecordNumber;
}
RT_API_ATTRS void ConnectionState::HandleAbsolutePosition(std::int64_t n) {
positionInRecord = std::max(n, std::int64_t{0}) + leftTabLimit.value_or(0);
}
RT_API_ATTRS void ConnectionState::HandleRelativePosition(std::int64_t n) {
positionInRecord = std::max(leftTabLimit.value_or(0), positionInRecord + n);
}
SavedPosition::SavedPosition(IoStatementState &io) : io_{io} {
ConnectionState &conn{io_.GetConnectionState()};
saved_ = conn;

View File

@@ -169,17 +169,18 @@ static inline RT_API_ATTRS char32_t GetRadixPointChar(const DataEdit &edit) {
// Prepares input from a field, and returns the sign, if any, else '\0'.
static RT_API_ATTRS char ScanNumericPrefix(IoStatementState &io,
const DataEdit &edit, Fortran::common::optional<char32_t> &next,
Fortran::common::optional<int> &remaining) {
remaining = io.CueUpInput(edit);
next = io.NextInField(remaining, edit);
Fortran::common::optional<int> &remaining,
IoStatementState::FastAsciiField *fastField = nullptr) {
remaining = io.CueUpInput(edit, fastField);
next = io.NextInField(remaining, edit, fastField);
char sign{'\0'};
if (next) {
if (*next == '-' || *next == '+') {
sign = *next;
if (!edit.IsListDirected()) {
io.SkipSpaces(remaining);
io.SkipSpaces(remaining, fastField);
}
next = io.NextInField(remaining, edit);
next = io.NextInField(remaining, edit, fastField);
}
}
return sign;
@@ -213,17 +214,18 @@ RT_API_ATTRS bool EditIntegerInput(IoStatementState &io, const DataEdit &edit,
}
Fortran::common::optional<int> remaining;
Fortran::common::optional<char32_t> next;
char sign{ScanNumericPrefix(io, edit, next, remaining)};
auto fastField{io.GetUpcomingFastAsciiField()};
char sign{ScanNumericPrefix(io, edit, next, remaining, &fastField)};
if (sign == '-' && !isSigned) {
io.GetIoErrorHandler().SignalError("Negative sign in UNSIGNED input field");
return false;
}
common::UnsignedInt128 value{0};
common::uint128_t value{0};
bool any{!!sign};
bool overflow{false};
const char32_t comma{GetSeparatorChar(edit)};
static constexpr auto maxu128{~common::UnsignedInt128{0}};
for (; next; next = io.NextInField(remaining, edit)) {
static constexpr auto maxu128{~common::uint128_t{0}};
for (; next; next = io.NextInField(remaining, edit, &fastField)) {
char32_t ch{*next};
if (ch == ' ' || ch == '\t') {
if (edit.modes.editingFlags & blankZero) {
@@ -243,7 +245,7 @@ RT_API_ATTRS bool EditIntegerInput(IoStatementState &io, const DataEdit &edit,
// input, like a few other Fortran compilers do.
// TODO: also process exponents? Some compilers do, but they obviously
// can't just be ignored.
while ((next = io.NextInField(remaining, edit))) {
while ((next = io.NextInField(remaining, edit, &fastField))) {
if (*next < '0' || *next > '9') {
break;
}
@@ -271,7 +273,7 @@ RT_API_ATTRS bool EditIntegerInput(IoStatementState &io, const DataEdit &edit,
return false;
}
if (isSigned) {
auto maxForKind{common::UnsignedInt128{1} << ((8 * kind) - 1)};
auto maxForKind{common::uint128_t{1} << ((8 * kind) - 1)};
overflow |= value >= maxForKind && (value > maxForKind || sign != '-');
} else {
auto maxForKind{maxu128 >> (((16 - kind) * 8) + (isSigned ? 1 : 0))};
@@ -287,7 +289,16 @@ RT_API_ATTRS bool EditIntegerInput(IoStatementState &io, const DataEdit &edit,
}
if (any || !io.GetIoErrorHandler().InError()) {
// The value is stored in the lower order bits on big endian platform.
// When memcpy, shift the value to the higher order bit.
// For memcpy, shift the value to the highest order bits.
#if USING_NATIVE_INT128_T
auto shft{static_cast<int>(sizeof value - kind)};
if (!isHostLittleEndian && shft >= 0) {
auto l{value << shft};
std::memcpy(n, &l, kind);
} else {
std::memcpy(n, &value, kind); // a blank field means zero
}
#else
auto shft{static_cast<int>(sizeof(value.low())) - kind};
// For kind==8 (i.e. shft==0), the value is stored in low_ in big endian.
if (!isHostLittleEndian && shft >= 0) {
@@ -296,6 +307,8 @@ RT_API_ATTRS bool EditIntegerInput(IoStatementState &io, const DataEdit &edit,
} else {
std::memcpy(n, &value, kind); // a blank field means zero
}
#endif
io.GotChar(fastField.got());
return true;
} else {
return false;
@@ -1070,7 +1083,7 @@ RT_API_ATTRS bool EditCharacterInput(IoStatementState &io, const DataEdit &edit,
readyBytes = io.GetNextInputBytes(input);
if (readyBytes == 0 ||
(readyBytes < remainingChars && edit.modes.nonAdvancing)) {
if (io.CheckForEndOfRecord(readyBytes)) {
if (io.CheckForEndOfRecord(readyBytes, connection)) {
if (readyBytes == 0) {
// PAD='YES' and no more data
Fortran::runtime::fill_n(x, lengthChars, ' ');

View File

@@ -1057,14 +1057,15 @@ bool IODEF(InputDescriptor)(Cookie cookie, const Descriptor &descriptor) {
}
bool IODEF(InputInteger)(Cookie cookie, std::int64_t &n, int kind) {
if (!cookie->CheckFormattedStmtType<Direction::Input>("InputInteger")) {
return false;
IoStatementState &io{*cookie};
if (io.BeginReadingRecord()) {
if (auto edit{io.GetNextDataEdit()}) {
return edit->descriptor == DataEdit::ListDirectedNullValue ||
EditIntegerInput(io, *edit, reinterpret_cast<void *>(&n), kind,
/*isSigned=*/true);
}
}
StaticDescriptor<0> staticDescriptor;
Descriptor &descriptor{staticDescriptor.descriptor()};
descriptor.Establish(
TypeCategory::Integer, kind, reinterpret_cast<void *>(&n), 0);
return descr::DescriptorIO<Direction::Input>(*cookie, descriptor);
return false;
}
bool IODEF(InputReal32)(Cookie cookie, float &x) {

View File

@@ -596,7 +596,7 @@ ExternalFileUnit *IoStatementState::GetExternalFileUnit() const {
[](auto &x) { return x.get().GetExternalFileUnit(); }, u_);
}
Fortran::common::optional<char32_t> IoStatementState::GetCurrentChar(
Fortran::common::optional<char32_t> IoStatementState::GetCurrentCharSlow(
std::size_t &byteCount) {
const char *p{nullptr};
std::size_t bytes{GetNextInputBytes(p)};
@@ -628,12 +628,24 @@ Fortran::common::optional<char32_t> IoStatementState::GetCurrentChar(
}
}
IoStatementState::FastAsciiField IoStatementState::GetUpcomingFastAsciiField() {
ConnectionState &connection{GetConnectionState()};
if (!connection.isUTF8 && connection.internalIoCharKind <= 1) {
const char *p{nullptr};
if (std::size_t bytes{GetNextInputBytes(p)}) {
return FastAsciiField(connection, p, bytes);
}
}
return FastAsciiField(connection);
}
Fortran::common::optional<char32_t> IoStatementState::NextInField(
Fortran::common::optional<int> &remaining, const DataEdit &edit) {
Fortran::common::optional<int> &remaining, const DataEdit &edit,
FastAsciiField *field) {
std::size_t byteCount{0};
if (!remaining) { // Stream, list-directed, NAMELIST, &c.
if (auto next{GetCurrentChar(byteCount)}) {
if (edit.width.value_or(0) == 0) {
if (auto next{GetCurrentChar(byteCount, field)}) {
if ((*next < '0' || *next == ';') && edit.width.value_or(0) == 0) {
// list-directed, NAMELIST, I0 &c., or width-free I/G:
// check for separator character
switch (*next) {
@@ -667,21 +679,30 @@ Fortran::common::optional<char32_t> IoStatementState::NextInField(
break;
}
}
HandleRelativePosition(byteCount);
GotChar(byteCount);
if (field) {
field->Advance(1, byteCount);
} else {
HandleRelativePosition(byteCount);
GotChar(byteCount);
}
return next;
}
} else if (*remaining > 0) {
if (auto next{GetCurrentChar(byteCount)}) {
if (auto next{GetCurrentChar(byteCount, field)}) {
if (byteCount > static_cast<std::size_t>(*remaining)) {
return Fortran::common::nullopt;
}
*remaining -= byteCount;
HandleRelativePosition(byteCount);
GotChar(byteCount);
if (field) {
field->Advance(1, byteCount);
} else {
HandleRelativePosition(byteCount);
GotChar(byteCount);
}
return next;
}
if (CheckForEndOfRecord(0)) { // do padding
if (CheckForEndOfRecord(0,
field ? field->connection() : GetConnectionState())) { // do padding
--*remaining;
return Fortran::common::optional<char32_t>{' '};
}
@@ -689,8 +710,8 @@ Fortran::common::optional<char32_t> IoStatementState::NextInField(
return Fortran::common::nullopt;
}
bool IoStatementState::CheckForEndOfRecord(std::size_t afterReading) {
const ConnectionState &connection{GetConnectionState()};
bool IoStatementState::CheckForEndOfRecord(
std::size_t afterReading, const ConnectionState &connection) {
if (!connection.IsAtEOF()) {
if (auto length{connection.EffectiveRecordLength()}) {
if (connection.positionInRecord +
@@ -799,7 +820,6 @@ Fortran::common::optional<DataEdit>
ListDirectedStatementState<Direction::Input>::GetNextDataEdit(
IoStatementState &io, int maxRepeat) {
// N.B. list-directed transfers cannot be nonadvancing (C1221)
ConnectionState &connection{io.GetConnectionState()};
DataEdit edit;
edit.descriptor = DataEdit::ListDirected;
edit.repeat = 1; // may be overridden below
@@ -840,14 +860,15 @@ ListDirectedStatementState<Direction::Input>::GetNextDataEdit(
imaginaryPart_ = true;
edit.descriptor = DataEdit::ListDirectedImaginaryPart;
}
auto ch{io.GetNextNonBlank(byteCount)};
auto fastField{io.GetUpcomingFastAsciiField()};
auto ch{io.GetNextNonBlank(byteCount, &fastField)};
if (ch && *ch == comma && eatComma_) {
// Consume comma & whitespace after previous item.
// This includes the comma between real and imaginary components
// in list-directed/NAMELIST complex input.
// (When DECIMAL='COMMA', the comma is actually a semicolon.)
io.HandleRelativePosition(byteCount);
ch = io.GetNextNonBlank(byteCount);
fastField.Advance(0, byteCount);
ch = io.GetNextNonBlank(byteCount, &fastField);
}
eatComma_ = true;
if (!ch) {
@@ -865,8 +886,9 @@ ListDirectedStatementState<Direction::Input>::GetNextDataEdit(
if (imaginaryPart_) { // can't repeat components
return edit;
}
if (*ch >= '0' && *ch <= '9') { // look for "r*" repetition count
auto start{connection.positionInRecord};
if (*ch >= '0' && *ch <= '9' && fastField.MightHaveAsterisk()) {
// look for "r*" repetition count
auto start{fastField.connection().positionInRecord};
int r{0};
do {
static auto constexpr clamp{(std::numeric_limits<int>::max() - '9') / 10};
@@ -875,12 +897,12 @@ ListDirectedStatementState<Direction::Input>::GetNextDataEdit(
break;
}
r = 10 * r + (*ch - '0');
io.HandleRelativePosition(byteCount);
ch = io.GetCurrentChar(byteCount);
fastField.Advance(0, byteCount);
ch = io.GetCurrentChar(byteCount, &fastField);
} while (ch && *ch >= '0' && *ch <= '9');
if (r > 0 && ch && *ch == '*') { // subtle: r must be nonzero
io.HandleRelativePosition(byteCount);
ch = io.GetCurrentChar(byteCount);
fastField.Advance(0, byteCount);
ch = io.GetCurrentChar(byteCount, &fastField);
if (ch && *ch == '/') { // r*/
hitSlash_ = true;
edit.descriptor = DataEdit::ListDirectedNullValue;
@@ -895,12 +917,12 @@ ListDirectedStatementState<Direction::Input>::GetNextDataEdit(
repeatPosition_.emplace(io);
}
} else { // not a repetition count, just an integer value; rewind
connection.positionInRecord = start;
fastField.connection().positionInRecord = start;
}
}
if (!imaginaryPart_ && ch && *ch == '(') {
realPart_ = true;
io.HandleRelativePosition(byteCount);
fastField.connection().HandleRelativePosition(byteCount);
edit.descriptor = DataEdit::ListDirectedRealPart;
}
return edit;

View File

@@ -135,6 +135,13 @@ bool ExternalFileUnit::Receive(char *data, std::size_t bytes,
std::size_t ExternalFileUnit::GetNextInputBytes(
const char *&p, IoErrorHandler &handler) {
RUNTIME_CHECK(handler, direction_ == Direction::Input);
if (access == Access::Sequential &&
positionInRecord < recordLength.value_or(positionInRecord)) {
// Fast path for variable-length formatted input: the whole record
// must be in frame as a result of newline detection for record length.
p = Frame() + recordOffsetInFrame_ + positionInRecord;
return *recordLength - positionInRecord;
}
std::size_t length{1};
if (auto recl{EffectiveRecordLength()}) {
if (positionInRecord < *recl) {
@@ -443,7 +450,6 @@ void ExternalFileUnit::Rewind(IoErrorHandler &handler) {
DoImpliedEndfile(handler);
SetPosition(0);
currentRecordNumber = 1;
leftTabLimit.reset();
anyWriteSinceLastPositioning_ = false;
}
}
@@ -455,6 +461,8 @@ void ExternalFileUnit::SetPosition(std::int64_t pos) {
directAccessRecWasSet_ = true;
}
BeginRecord();
beganReadingRecord_ = false; // for positioning after nonadvancing input
leftTabLimit.reset();
}
void ExternalFileUnit::Sought(std::int64_t zeroBasedPos) {

View File

@@ -278,9 +278,11 @@ using SignedInt128 = Int128<true>;
#if !AVOID_NATIVE_UINT128_T && (defined __GNUC__ || defined __clang__) && \
defined __SIZEOF_INT128__
#define USING_NATIVE_INT128_T 1
using uint128_t = __uint128_t;
using int128_t = __int128_t;
#else
#undef USING_NATIVE_INT128_T
using uint128_t = UnsignedInt128;
using int128_t = SignedInt128;
#endif