frumos
Hello Elixir community,
I would like to share with you some results of my first 2 months of Elixir learning and ask important question about Elixir language performance as such.
I’ve made some POC application and was able to run it on production-like host (AWS c5.9xl - 96 cores, 70Gb of RAM) and I got about 3-3.5 time worse tps comparing to our current prod metrics. To troubleshoot I just decided make some very rudimentary tests of the main logic which is key in our business - the logic is very simple:
- iterate over files (compressed JSONs or plain CSVs) and
- line by line make some map transformation and
- perform aggregation by applying a sum function for the decimal value on string key
Simple enough, however the only feature of our business is the volume - it is 1 quadrillion (10^15) records per month what I need to account which is ~380 * 10^6 tps. We have such applications which are handling this volume in prod ATM.
For Elixir capability evaluation I took 2 files a plain csv what I need to aggregate (reduce by sum function) - the small (~1000 records) and big (11.7 million records)
Please see snippet of this file
HeaderLength=8
DatasetName=xxxBillingHourly
CreationTimestamp=1561942800000
StartRangeMarker=2019-07-01-01-00-00
EndRangeMarker=2019-07-01-02-00-00
FieldSpec=field-1,field-2,field-3,field-4,field-5,field-6,field-7,field-8,field-9,startTime,endTime,value
KeySpec=field-1,field-2,field-3,field-4,field-5,field-6,field-7,field-8,field-9,startTime,endTime
DataSources=someS3bucket
007931482,abcStore,abcStore,GetPar,GetPar,,us-west-1,,,2018-07-01 01:00:00,2018-07-01 02:00:00,10
016299379,abcEC2,abcEC2,Hours,Gateway,,,arn:aws:someARN,,2018-07-01 01:00:00,2018-07-01 02:00:00,10
018870929,abcLambda,abcLambda,Second,Invoke,,,arn:aws:some-function,,2018-07-01 01:00:00,2018-07-01 02:00:00,59.35000000000002086
.
.
and used Elixir and Java programs to compare with each other (Java is current language we use).
Bellow a listing of two programs which are doing the same things.
Java
@Test
public void konaAggregate() throws IOException {
long start = System.currentTimeMillis();
System.out.println("Start");
String fName = "/home/temp/1/real_kona";
Path totalFilePath = Paths.get(fName + "_java_aggregated");
Stream<Entry<String, BigDecimal>> stream = Files
.lines(Paths.get(fName))
.skip(9)
.map(l -> {
String[] parts = l.split(",");
return Tuple.of(String.join(",", parts[0], parts[1], parts[2], parts[3], parts[4]), new BigDecimal(parts[11]));
})
.collect(
Collectors.groupingBy(
Tuple2::_1,
TreeMap::new,
Collectors.mapping(Tuple2::_2, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))
)
)
.entrySet()
.stream();
try (BufferedWriter writer = Files.newBufferedWriter(totalFilePath)) {
stream.forEach(line -> {
String record = line.getKey() + "," + line.getValue().toPlainString();
try {
writer.write(record);
writer.newLine();
} catch (IOException e) {
// ignore
}
});
}
long end = System.currentTimeMillis();
System.out.println("Total time: " + (end - start));
}
Elixir
def aggregate_kona() do
IO.puts("Start")
kona = "/home/temp/1/real_kona"
output_path = kona <> "_elixir_aggregated"
start = :os.system_time(:millisecond)
kona
|> File.stream!
|> Stream.map(&String.split(&1, "\n"))
|> Stream.drop(9)
|> Stream.map(&(&1 |> hd))
|> Stream.map(&parse_granular(&1))
|> Enum.reduce(
%{},
fn %{record_key: key, record_value: value}, acc ->
Map.update(acc, key, value, &Decimal.add(&1, value))
end
)
|> Stream.map(&((elem(&1, 0) <> "," <> Decimal.to_string(elem(&1, 1))) <> "\n"))
|> Stream.into(File.stream!(output_path, [:write, :utf8]))
|> Stream.run()
stop = :os.system_time(:millisecond)
IO.puts("Total: #{stop - start}")
end
def parse_granular(record) do
[p,
pr,
cpc,
ut,
op,
_,
_,
_,
_,
_,
_,
value] =
record |> String.split(",")
%{record_key: p <> "," <> pr <> "," <> cpc <> "," <> ut <> "," <> op, record_value: Decimal.new(value)}
end
Let me give you results:
For small file which is 1K records there is no issues and both are (Elixir is faster) latency is 50 milliseconds
But for the big file (11.7 million records and ~2Gb size)
Java gives 16 seconds latency and is able to aggregate file
Elixir was running for ~20 minutes with no produced result and I just terminated an iex session
To get somewhere, I made flow in Elixir even simpler, I commented a reduce part of the flow and made a run. It gave me latency ~115 seconds. Java version for such work coped for ~13 seconds.
So the question to Elixir community, could you review please my task, implementation whether I have any obvious mistakes in Elixir part and also why Elixir’s performance is such that I simply can not accept it?
Thank you.
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #elixirconf-eu
- #metaprogramming
- #hex










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
jola
So… a couple of things. First,
File.stream!is already splitting the input into lines by default, but you thenStream.map(&String.split(&1, "\n"))again on each line, which does nothing.If you need to do a large number of updates on a
Map, consider switching to ETS. It will perform much better with large amounts of data, since it has constant time access and isn’t garbage collected.There are some other things I’d try that might help too, like replacing this
&((elem(&1, 0) <> "," <> Decimal.to_string(elem(&1, 1))) <> "\n")withfn {key, value} -> key <> "," <> Decimal.to_string(elem(&1, 1)) <> "\n" end.You’re also building a lot of strings, which in eg the JVM is automatically optimized. The BEAM doesn’t optimize that automatically, but there’s this concept of “iolists” that let you manually optimize it.
I’d suggest taking a look at a similar thread posted before that has a lot of tips Erlang/Elixir string performance - can this be improved?
I also wrote an article about this which isn’t completely up to date but collects most of the improvements from the thread Elixir String Processing Optimization | jola.dev and it has examples of eg using ETS instead of
Mapand using iolists.frumos
Thank you. Let me revisit impl with suggestions you gave and I’ll be back with (hopefully) new results.
frumos
Sorry could you help to rewrite a reduce function using ETS from the example I have? I desperately spent 3 hours doing that with no result and getting
This is what I am trying
Erlang doc is not clear at all.
Thank you.
7stud
Here’s an example:
In iex:
jola
You’re using
update_counter, which only supports updating integers, but your value is some struct. It is in the documentation, but I also think erlang documentation is hard to read.You’ll need a combination of
lookupandinsert. Something likeNote that I have no idea what
Decimalis, so I don’t know how to create them or how to add them together, so I just used+, you’ll have to replace that. I haven’t tested the code.frumos
Thank Jola, I spent some more time an came up with similar solution myself and I have huge (but not the best) result
Here my implementation
with that I can now DO aggregate the big (2Gb) file with latency ~135 sec, but it is still ~8x slower than my java version. Could you please take one more look to see if I can do any better?
Thank you for you time and help!
frumos
with your version of reduction function got 15 sec boost more and final latency now is ~120 sec. But my question for more review still valid. Thank you
DiodonHystrix
I’m not that sure, but…
I’m not surprised that this version is slower, because elixir/erlang is slower than java in general. To outperform java you need to use currency, which is not used in this case. Stream just helps with memory usage AFAIK.
You probably should look into Flow — Flow v1.2.4, which will parallelize your computations.
Disclaimer: I may be talking bs…
david_ex
That’s not entirely true, stricto senso: streams allow composing operations so the collection is iterated over only once. In other words, if you want to apply multiple transformations to a collection of items, if you use
Mapthe collection will be traversed once per transformation, but if you useStreamall the transformations will be “collected” into a single operation and only then will the collection be traversed (and only once).NobbZ
Yes, and this is why it helps with memory consumption, we have to pay this though.
Streams have an additional computational overhead and often slower than their eager counterparts.But as we have an arbitrary large input file, it sounds as if the price is worth to be paid.
Anyway, I doubt as well, that the BEAM version will be able to be as performant as the Java version.
The BEAM version involves a lot of copying data back and forth between the process and the ETS, which in the Java Version is probably mutated in place.
Still, I’d like to give some more suggestions:
setis somewhat costly on insert/update, as it always has to scan the table for the key, but maybe I’m missremembering things from the talk (it was briefly mentioned in the talk about how phoenix got to 2M active connections)String.slice/2/3to get the string-id and value.:ets.tab2listis more performant.