Skip to content

Commit 9223c13

Browse files
apacheGH-48160: [C++][Gandiva] Pass CPU attributes to LLVM (apache#48161) (#120)
The CPU attributes are not passed to the LLVM layer, which means potential optimizations could be missed leading to inefficient code. This feature was lost as part of the refactoring in apache@83cba25 I also discovered a bug with decimal alignments that was exposed by this change and was only reproducible in our test environment. Pass the CPU attributes to the LLVM code generation, and a unit test. Fix the 16 bit vs 8 bit decimal alignment problem. This was causing a crash sometimes on certain architectures with certain queries. Added a unit test. Yes. No. * GitHub Issue: apache#48160 Lead-authored-by: Logan Riggs <logan.riggs@dremio.com> Signed-off-by: Sutou Kouhei <kou@clear-code.com> Co-authored-by: Logan Riggs <logan.riggs@dremio.com> Co-authored-by: lriggs <logan.riggs@gmail.com>
1 parent f5c2824 commit 9223c13

3 files changed

Lines changed: 266 additions & 3 deletions

File tree

cpp/src/gandiva/llvm_generator.cc

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,13 @@ Status LLVMGenerator::CodeGenExprValue(DexPtr value_expr, int buffer_count,
401401
auto output_type_id = output->Type()->id();
402402
if (output_type_id == arrow::Type::BOOL) {
403403
SetPackedBitValue(output_ref, loop_var, output_value->data());
404-
} else if (arrow::is_primitive(output_type_id) ||
405-
output_type_id == arrow::Type::DECIMAL) {
404+
} else if (output_type_id == arrow::Type::DECIMAL) {
405+
// Arrow decimal128 data is only 8-byte aligned, not 16-byte aligned.
406+
// Use CreateAlignedStore with 8-byte alignment to match Arrow's actual alignment.
407+
auto slot_offset =
408+
builder->CreateGEP(types()->IRType(output_type_id), output_ref, loop_var);
409+
builder->CreateAlignedStore(output_value->data(), slot_offset, llvm::MaybeAlign(8));
410+
} else if (arrow::is_primitive(output_type_id)) {
406411
auto slot_offset =
407412
builder->CreateGEP(types()->IRType(output_type_id), output_ref, loop_var);
408413
builder->CreateStore(output_value->data(), slot_offset);
@@ -643,7 +648,12 @@ void LLVMGenerator::Visitor::Visit(const VectorReadFixedLenValueDex& dex) {
643648

644649
case arrow::Type::DECIMAL: {
645650
auto slot_offset = builder->CreateGEP(types->i128_type(), slot_ref, slot_index);
646-
slot_value = builder->CreateLoad(types->i128_type(), slot_offset, dex.FieldName());
651+
// Arrow decimal128 data is only 8-byte aligned, not 16-byte aligned.
652+
// Using CreateLoad with default alignment (16 for i128) causes crashes on
653+
// misaligned data. Use CreateAlignedLoad with 8-byte alignment to match Arrow's
654+
// actual alignment.
655+
slot_value = builder->CreateAlignedLoad(
656+
types->i128_type(), slot_offset, llvm::MaybeAlign(8), false, dex.FieldName());
647657
lvalue = generator_->BuildDecimalLValue(slot_value, dex.FieldType());
648658
break;
649659
}

cpp/src/gandiva/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ add_gandiva_test(projector-test
2121
binary_test.cc
2222
boolean_expr_test.cc
2323
date_time_test.cc
24+
decimal_alignment_test.cc
2425
decimal_single_test.cc
2526
decimal_test.cc
2627
filter_project_test.cc
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
// Test for decimal128 alignment issue fix.
19+
// Arrow decimal128 data may be 8-byte aligned but not 16-byte aligned.
20+
// This test verifies that Gandiva handles such data correctly.
21+
22+
#include <gtest/gtest.h>
23+
24+
#include "arrow/array/array_decimal.h"
25+
#include "arrow/array/builder_primitive.h"
26+
#include "arrow/buffer.h"
27+
#include "arrow/memory_pool.h"
28+
#include "arrow/status.h"
29+
#include "arrow/util/decimal.h"
30+
31+
#include "gandiva/decimal_type_util.h"
32+
#include "gandiva/projector.h"
33+
#include "gandiva/tests/test_util.h"
34+
#include "gandiva/tree_expr_builder.h"
35+
36+
using arrow::Decimal128;
37+
38+
namespace gandiva {
39+
40+
class TestDecimalAlignment : public ::testing::Test {
41+
public:
42+
void SetUp() { pool_ = arrow::default_memory_pool(); }
43+
44+
protected:
45+
arrow::MemoryPool* pool_;
46+
};
47+
48+
// Create a decimal128 array with data at a specific alignment offset
49+
// This simulates the real-world scenario where Arrow data from external sources
50+
// (like JNI/Java) may not be 16-byte aligned.
51+
std::shared_ptr<arrow::Array> MakeMisalignedDecimalArray(
52+
const std::shared_ptr<arrow::Decimal128Type>& type,
53+
const std::vector<Decimal128>& values, int alignment_offset) {
54+
// Allocate buffer with extra space for misalignment
55+
int64_t data_size = values.size() * 16; // 16 bytes per Decimal128
56+
int64_t buffer_size = data_size + 16; // Extra space for offset
57+
58+
std::shared_ptr<arrow::Buffer> buffer;
59+
ARROW_EXPECT_OK(arrow::AllocateBuffer(buffer_size).Value(&buffer));
60+
61+
// Calculate the starting offset to achieve desired alignment
62+
// We want the data to be 8-byte aligned but NOT 16-byte aligned
63+
uint8_t* raw_data = buffer->mutable_data();
64+
uintptr_t addr = reinterpret_cast<uintptr_t>(raw_data);
65+
66+
// Find offset to get to 8-byte aligned but not 16-byte aligned address
67+
int offset_to_8 = (8 - (addr % 8)) % 8;
68+
int current_16_alignment = (addr + offset_to_8) % 16;
69+
70+
int final_offset;
71+
if (alignment_offset == 8) {
72+
// Want 8-byte aligned but NOT 16-byte aligned
73+
if (current_16_alignment == 0) {
74+
final_offset = offset_to_8 + 8; // Add 8 to break 16-byte alignment
75+
} else {
76+
final_offset = offset_to_8;
77+
}
78+
} else {
79+
// Want 16-byte aligned
80+
final_offset = (16 - (addr % 16)) % 16;
81+
}
82+
83+
// Copy decimal values to the offset location
84+
uint8_t* data_start = raw_data + final_offset;
85+
for (size_t i = 0; i < values.size(); i++) {
86+
memcpy(data_start + i * 16, values[i].ToBytes().data(), 16);
87+
}
88+
89+
// Verify alignment
90+
uintptr_t data_addr = reinterpret_cast<uintptr_t>(data_start);
91+
EXPECT_EQ(data_addr % 8, 0) << "Data should be 8-byte aligned";
92+
if (alignment_offset == 8) {
93+
EXPECT_NE(data_addr % 16, 0) << "Data should NOT be 16-byte aligned";
94+
}
95+
96+
// Create a sliced buffer starting at our offset
97+
auto sliced_buffer = arrow::SliceBuffer(buffer, final_offset, data_size);
98+
99+
// Create validity buffer (all valid)
100+
std::shared_ptr<arrow::Buffer> validity_buffer;
101+
ARROW_EXPECT_OK(arrow::AllocateBuffer((values.size() + 7) / 8).Value(&validity_buffer));
102+
memset(validity_buffer->mutable_data(), 0xFF, validity_buffer->size());
103+
104+
// Create the array with our misaligned data buffer
105+
auto array_data = arrow::ArrayData::Make(type, static_cast<int64_t>(values.size()),
106+
{validity_buffer, sliced_buffer});
107+
108+
return std::make_shared<arrow::Decimal128Array>(array_data);
109+
}
110+
111+
// Test that decimal operations work correctly with 8-byte aligned (but not 16-byte
112+
// aligned) data
113+
TEST_F(TestDecimalAlignment, TestMisalignedDecimalSubtract) {
114+
constexpr int32_t precision = 38;
115+
constexpr int32_t scale = 17;
116+
auto decimal_type = std::make_shared<arrow::Decimal128Type>(precision, scale);
117+
auto field_a = arrow::field("a", decimal_type);
118+
auto field_b = arrow::field("b", decimal_type);
119+
auto schema = arrow::schema({field_a, field_b});
120+
121+
Decimal128TypePtr output_type;
122+
auto status = DecimalTypeUtil::GetResultType(
123+
DecimalTypeUtil::kOpSubtract, {decimal_type, decimal_type}, &output_type);
124+
ASSERT_OK(status);
125+
126+
auto res = arrow::field("res", output_type);
127+
auto node_a = TreeExprBuilder::MakeField(field_a);
128+
auto node_b = TreeExprBuilder::MakeField(field_b);
129+
auto subtract =
130+
TreeExprBuilder::MakeFunction("subtract", {node_a, node_b}, output_type);
131+
auto expr = TreeExprBuilder::MakeExpression(subtract, res);
132+
133+
std::shared_ptr<Projector> projector;
134+
status = Projector::Make(schema, {expr}, TestConfiguration(), &projector);
135+
ASSERT_OK(status);
136+
137+
// Create test data
138+
std::vector<Decimal128> values_a = {Decimal128(100), Decimal128(200), Decimal128(300)};
139+
std::vector<Decimal128> values_b = {Decimal128(10), Decimal128(20), Decimal128(30)};
140+
141+
// Create arrays with 8-byte alignment (but NOT 16-byte aligned)
142+
auto array_a = MakeMisalignedDecimalArray(decimal_type, values_a, 8);
143+
auto array_b = MakeMisalignedDecimalArray(decimal_type, values_b, 8);
144+
145+
auto in_batch = arrow::RecordBatch::Make(schema, 3, {array_a, array_b});
146+
147+
// This should NOT crash even with misaligned data
148+
arrow::ArrayVector outputs;
149+
status = projector->Evaluate(*in_batch, pool_, &outputs);
150+
ASSERT_OK(status);
151+
152+
// Verify results: 100-10=90, 200-20=180, 300-30=270
153+
auto result = std::dynamic_pointer_cast<arrow::Decimal128Array>(outputs[0]);
154+
ASSERT_NE(result, nullptr);
155+
EXPECT_EQ(result->length(), 3);
156+
}
157+
158+
// Create a misaligned output buffer for decimal128
159+
std::shared_ptr<arrow::ArrayData> MakeMisalignedDecimalOutput(
160+
const std::shared_ptr<arrow::Decimal128Type>& type, int64_t num_records,
161+
int alignment_offset) {
162+
// Allocate data buffer with extra space for misalignment
163+
int64_t data_size = num_records * 16; // 16 bytes per Decimal128
164+
int64_t buffer_size = data_size + 16; // Extra space for offset
165+
166+
std::shared_ptr<arrow::Buffer> buffer;
167+
ARROW_EXPECT_OK(arrow::AllocateBuffer(buffer_size).Value(&buffer));
168+
169+
uint8_t* raw_data = const_cast<uint8_t*>(buffer->data());
170+
uintptr_t addr = reinterpret_cast<uintptr_t>(raw_data);
171+
172+
// Find offset to get to 8-byte aligned but not 16-byte aligned address
173+
int offset_to_8 = (8 - (addr % 8)) % 8;
174+
int current_16_alignment = (addr + offset_to_8) % 16;
175+
176+
int final_offset;
177+
if (alignment_offset == 8) {
178+
if (current_16_alignment == 0) {
179+
final_offset = offset_to_8 + 8;
180+
} else {
181+
final_offset = offset_to_8;
182+
}
183+
} else {
184+
final_offset = (16 - (addr % 16)) % 16;
185+
}
186+
187+
// Verify alignment
188+
uintptr_t data_addr = reinterpret_cast<uintptr_t>(raw_data + final_offset);
189+
EXPECT_EQ(data_addr % 8, 0) << "Data should be 8-byte aligned";
190+
if (alignment_offset == 8) {
191+
EXPECT_NE(data_addr % 16, 0) << "Data should NOT be 16-byte aligned";
192+
}
193+
194+
auto sliced_buffer = arrow::SliceBuffer(buffer, final_offset, data_size);
195+
196+
// Create validity buffer
197+
int64_t bitmap_size = (num_records + 7) / 8;
198+
std::shared_ptr<arrow::Buffer> validity_buffer;
199+
ARROW_EXPECT_OK(arrow::AllocateBuffer(bitmap_size).Value(&validity_buffer));
200+
memset(const_cast<uint8_t*>(validity_buffer->data()), 0xFF, validity_buffer->size());
201+
202+
return arrow::ArrayData::Make(type, num_records, {validity_buffer, sliced_buffer});
203+
}
204+
205+
// Test that decimal STORES work correctly with 8-byte aligned (but not 16-byte aligned)
206+
// output
207+
TEST_F(TestDecimalAlignment, TestMisalignedDecimalStore) {
208+
constexpr int32_t precision = 38;
209+
constexpr int32_t scale = 17;
210+
auto decimal_type = std::make_shared<arrow::Decimal128Type>(precision, scale);
211+
auto field_a = arrow::field("a", decimal_type);
212+
auto field_b = arrow::field("b", decimal_type);
213+
auto schema = arrow::schema({field_a, field_b});
214+
215+
Decimal128TypePtr output_type;
216+
auto status = DecimalTypeUtil::GetResultType(
217+
DecimalTypeUtil::kOpSubtract, {decimal_type, decimal_type}, &output_type);
218+
ASSERT_OK(status);
219+
220+
auto res = arrow::field("res", output_type);
221+
auto node_a = TreeExprBuilder::MakeField(field_a);
222+
auto node_b = TreeExprBuilder::MakeField(field_b);
223+
auto subtract =
224+
TreeExprBuilder::MakeFunction("subtract", {node_a, node_b}, output_type);
225+
auto expr = TreeExprBuilder::MakeExpression(subtract, res);
226+
227+
std::shared_ptr<Projector> projector;
228+
status = Projector::Make(schema, {expr}, TestConfiguration(), &projector);
229+
ASSERT_OK(status);
230+
231+
// Create ALIGNED input arrays (using standard Arrow allocation)
232+
auto array_a = MakeArrowArrayDecimal(
233+
decimal_type, {Decimal128(100), Decimal128(200), Decimal128(300)},
234+
{true, true, true});
235+
auto array_b = MakeArrowArrayDecimal(
236+
decimal_type, {Decimal128(10), Decimal128(20), Decimal128(30)}, {true, true, true});
237+
238+
auto in_batch = arrow::RecordBatch::Make(schema, 3, {array_a, array_b});
239+
240+
// Create MISALIGNED output buffer (8-byte aligned but NOT 16-byte aligned)
241+
auto output_data = MakeMisalignedDecimalOutput(output_type, 3, 8);
242+
243+
// This should NOT crash even with misaligned output buffer
244+
status = projector->Evaluate(*in_batch, {output_data});
245+
ASSERT_OK(status);
246+
247+
// Verify the output was written correctly
248+
auto result = std::make_shared<arrow::Decimal128Array>(output_data);
249+
EXPECT_EQ(result->length(), 3);
250+
}
251+
252+
} // namespace gandiva

0 commit comments

Comments
 (0)