You would think that C++ would run laps around C# for a science project, that taking the time to do things right in C++ would pay off big time. We shall see...
Introduction
A little background.
A million years ago, I was a stellar math and physics student. Let's say I peaked early...
College math and physics were much harder than expected. I ended up meeting my wife and learning C++. Mission accomplished!
Aside: When I entered college, they taught new students like myself C++. When I left, they were teaching noobs Java. Now they teach them Python. Alas.
So even though physics didn't work out in college, I still dreamt of crazy delusional things like nuclear fusion. I wanted to get on a team doing that sort of thing, to save the world from wars over fossil fuels. Yep, crazy and delusional.
Setting Off To Do Science
So I took a third semester physics class from the local community college. The coolest experiment was "Simple Harmonic Motion On A String." In this experiment, there's a string suspended above a table. At one end, the string is held fixed with a weight applied, creating tension. At the other end of the table, the string is passed over an oscillator that moves that end of the string up and down. The oscillator's motion can be dialled in to a specific frequency, and, then, magic: Patterns called standing waves appear on the vibrating string. With what are called resonant frequencies, the standing waves become pronounced, and large patterns emerge in the vibrating string. This fascinated the hell out of me.
Many great diagrams here
As a software guy, I was inspired to take a diagram from the textbook and try to make a simulation of the standing waves string experiment. At the time, Microsoft was not giving away the crown jewels for free the way they have done with the Visual Studio 2017 and 2019 Community Editions. I wanted to do C++ programming, and the only way to do that back in the days of Visual Studio Express was .NET's C++/CLI, called Managed C++ at the time. I hope you like carrots! It had a form designer, and the .NET library was quite workable. So I set to work.
Weeks of development and tuning later, I had a working simulation. Tuning it took forever, for example my wife's revelation that the tension had to be quite high. There are a couple of these hard-fought magic numbers I had to hone in on to get it to where you can put in a frequency like 20 Hz and see a standing wave.
The User Interface
At the left, top to bottom, are a real-time view of the string, and snapshots of the string when it was at its most extreme amplitude (height), velocity (up and down), acceleration, and punch (not fruit, change in acceleration). For each string, in the bottom-left you see the strings height at that moment (in millimetres), in the top right, you see extreme value that was measured ("max =
"), and in the bottom right you see the elapsed time ("time =
") in the simulation when the maximum was recorded.
At the right are the simulation controls.
- Time Slice is how quickly the simulation runs, in parts of a millisecond. Too high and the simulation is not realistic. Too low and the simulation runs too slowly. Best to not go higher that 0.01 ms. You might go lower to validate that something you observed is not some artifact of simulation overload from running too fast.
- Simulation Speed controls the delay in the simulation's update loop. For any real study, you would run it at All Out for hours on end. You can use lower speeds to observe the string motion even slower.
- Tension / Root Note is tricky. The number you see there is a magic number arrived at tuning the string to resonate at 10 Hz. You can enter music notes, like C0, and then instead of frequencies you can enter other musical notes, and see how consonance and dissonance play out on the string. Mad science.
- Oscillators define the vibrations made at the ends of the strings. Not limited to a single physical oscillator with a single frequency, you can enter whatever you'd like for either side or both, each frequency on its own line. Don't get carried away, though. With too many frequencies, especially high ones, you start to see un-string-like behavior. It's a difficult point, that to find anything interesting you have to go to lengths that might mean that what you're seeing isn't realistic. You can try lowering the Time Slice and running the simulation for longer to see if what you found is realistic.
- Out of Phase sets the offset in the left versus the right oscillators. If you put the same frequency on the left and right, and set this to zero, the string will just move up and down and no standing waves will form. In the real world, I was able to prove this out with a string between two oscillators. Only when I reverse wired them did a standing wave form.
- Just Pulse and Just Half Pulse let you see how a standing wave forms in the very beginning with just one wave front moving down the string and echoing back.
- Run / Pause controls the simulation, starting or pausing or restarting it.
- Reset turns the clock back to zero and resets all the strings.
- Reset Maximums resets just the maximum strings. This is useful after running a simulation for a little while, to eliminate the weird stuff that happens early in a simulation, to reproduce a legitimate finding of interest, if there were such things.
- Copy Stats Header puts the stats columns on the clipboard to paste into a spreadsheet program.
- Copy Stats Values puts the stats values on the clipboard to paste into a spreadsheet program.
- stringshear.com has an online TypeScript version of this project that you can play with, no need to download or build anything, and some documentation similar to this.
You can tell you've got a strong standing wave when in the top-left panel, the black circles at either end of the string aren't moving up-and-down much, but the string is. That's the magic, the resonant frequencies, where the string resonates with the vibrations it's getting from its ends.
Build it and plug in the frequencies you've got here, and watch how, in slow motion, the string moves, and maximum strings are recorded. It's mesmerizing.
The science is interesting, but this is not a scientific journal. Let's talk code.
Moving On to the Code
Here are the projects in the attached code ZIP:
original
- C# port of the truly original C++/CLI WinForms program (from 2007!) app
- C# WinForms UI client runs the new client-server show sharpsvr
- C# HttpListener
server runs the .NET simulation sharplib
- C# class library implements the simulation, including inter-process communication seasvr
- C# HttpListener
server runs the C++ simulation sealib
- C++ DLL counterpart to sharplib
Here are the classes of interest, named and coded (mostly) the same in C# and C++ for easy comparison:
Simulation
- Runs the experiment, and does serialization and event handling with the app Stringy
- Well, I couldn't call it String! Manages serialization and updating Particle
- Our string effectively consists of 1,000 strung beads... particles sound cooler than beads ScopeTiming
- Performance tracking
Code Mechanics
In the original app, all the classes are in the one project. To do a performance comparison between C++ and C# - as I am prone to do! - I needed to run the simulators out of process, in a .NET server and a C++ server. You need to be able to leave the simulation running headless for hours on end at "All Out" speed, without having the app burning up the graphics card and adding lock contention to the simulation.
The general algorithm is...
- The simulation is always running, calling the
Update
function which simulates oscillators and updates strings and particles - The simulation receives settings from the app to apply to itself
- This simulation receives requests from the app to return its state (strings, maxes)
- The app has UI controls for sending settings to control the simulation
- The app has a timer that requests simulation state and updates the display
So my plan of attack was to follow these steps, validating that things still worked at each step:
- Pull non-UI bits out of the app into a class library,
sharplib
. - Introduce serialization of app commands and simulation state between the
sharplib
classes and the app UI. - Write a
HttpListener
server, sharpsvr
, and use a WebClient
in the app, and do out-of-process communication between the UI and the simulation. - Port the server and most of sharplib to C++ and iterate from there.
Let's dive in!
Performance Tracking with ScopeTiming - C#
using...
namespace...
public static class ScopeTiming
{
public static void Init(bool enable)
{
sm_enabled = enable;
}
public static Stopwatch StartTiming()
{
if (!sm_enabled)
return null;
return Stopwatch.StartNew();
}
public static void RecordScope(string scope, Stopwatch sw)
{
if (sw == null)
return;
var elapsedMs = sw.Elapsed;
lock (sm_timings)
{
Scope scopeObj;
if (!sm_timings.TryGetValue(scope, out scopeObj))
{
scopeObj = new Scope() { ScopeName = scope, Hits = 1, Allotted = elapsedMs };
sm_timings.Add(scope, scopeObj);
}
else
{
++scopeObj.Hits;
scopeObj.Allotted += elapsedMs;
}
}
sw.Restart();
}
public static string Summary
{
get
{
var sb = new List<string>();
lock (sm_timings)
{
foreach (Scope obj in sm_timings.Values)
{
if (obj.Hits == 0)
continue;
sb.Add
(
$"{obj.ScopeName} -> {obj.Hits} hits - " +
$"{Math.Round(obj.Allotted.TotalMilliseconds)} ms total -> " +
$"{Math.Round(obj.Allotted.TotalMilliseconds / obj.Hits, 5)} ms avg"
);
}
}
sb.Sort();
return string.Join("\n", sb);
}
}
public static void Clear()
{
lock (sm_timings)
sm_timings.Clear();
}
private static bool sm_enabled;
private class Scope
{
public string ScopeName;
public int Hits;
public TimeSpan Allotted;
}
private static Dictionary<string, Scope> sm_timings = new Dictionary<string, Scope>();
}
The Particle Class - C#
The Stringy
class has an array of Particle
s, so they better be struct
s for good data locality.
NOTE: ToString
turns out to be wicked fast, but FromString
does not have to be fast as it's app side, running at human time, so it's meant to be readable, not fast.
using System;
using System.Linq;
namespace StringShear
{
public struct Particle
{
public double x;
public double y;
public double vel;
public double acl;
public double punch;
public double nextNeighborFactor;
public Particle(double _x, double _y = 0, double _vel = 0,
double _acl = 0, double _punch = 0, double _next = 0)
{
x = _x;
y = _y;
vel = _vel;
acl = _acl;
punch = _punch;
nextNeighborFactor = _next;
}
public override string ToString()
{
return $"{x},{y},{vel},{acl},{punch},{nextNeighborFactor}";
}
public static Particle FromString(string str)
{
double[] vals =
str
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => double.Parse(x))
.ToArray();
if (vals.Length != 6)
throw new Exception("Particle parsing fails: " + str);
return new Particle(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]);
}
public double GetVal(int index)
{
switch (index)
{
case 0: return x;
case 1: return y;
case 2: return vel;
case 3: return acl;
case 4: return punch;
default: throw new Exception($"Invalid index to get: {index}");
}
}
public void Reset()
{
y = vel = acl = punch = nextNeighborFactor = 0.0;
}
public double SetPosY(double newPosY, double elapsedTime, double time)
{
double newDisplacement = (newPosY - y);
double newVel = newDisplacement / elapsedTime;
double newAcl = (newVel - vel) / elapsedTime;
if (time <= elapsedTime)
newAcl = 0.0;
double newPunch = (newAcl - acl) / elapsedTime;
if (time <= elapsedTime * 2.0)
newPunch = 0.0;
double workDone = newDisplacement * acl;
y = newPosY;
vel = newVel;
acl = newAcl;
punch = newPunch;
return Math.Abs(workDone);
}
}
}
The Particle Class - C++
Identical code to the C# version, but no ToString
/ FromString
serialization.
The String(y) Class - C#
using ...
namespace...
public class Stringy
{
Particle[] m_particles;
double m_length;
double m_maxPos;
double m_maxVel;
double m_maxAcl;
double m_maxPunch;
int m_maxPosIndex;
int m_maxVelIndex;
int m_maxAclIndex;
int m_maxPunchIndex;
bool m_waveDownAndBackYet;
Stopwatch m_sw = new Stopwatch();
public Stringy(Particle[] particles, double length)
{
m_length = length;
m_particles = (Particle[])particles.Clone();
}
public Stringy(int particleCount, double length)
{
m_length = length;
m_particles = new Particle[particleCount];
m_particles[0] = new Particle();
for (int i = 1; i < particleCount; ++i)
m_particles[i] = new Particle(m_length * 1.0 * i / (particleCount - 1));
}
public override string ToString()
{
Stopwatch sw = ScopeTiming.StartTiming();
StringBuilder sb = new StringBuilder();
sb.Append("particles:" + string.Join("|",
m_particles.Select(p => p.ToString())) + ";");
ScopeTiming.RecordScope("Stringy.ToString.Particles", sw);
sb.Append("length:" + m_length + ";");
sb.Append("maxPos:" + m_maxPos + ";");
sb.Append("maxVel:" + m_maxVel + ";");
sb.Append("maxAcl:" + m_maxAcl + ";");
sb.Append("maxPunch:" + m_maxPunch + ";");
sb.Append("maxPosIndex:" + m_maxPosIndex + ";");
sb.Append("maxVelIndex:" + m_maxVelIndex + ";");
sb.Append("maxAclIndex:" + m_maxAclIndex + ";");
sb.Append("maxPunchIndex:" + m_maxPunchIndex + ";");
sb.Append("startWork:" + m_startWork + ";");
sb.Append("endWork:" + m_endWork + ";");
sb.Append("maxStartWork:" + m_maxStartWork + ";");
sb.Append("maxEndWork:" + m_maxEndWork + ";");
ScopeTiming.RecordScope("Stringy.ToString.TheRest", sw);
string str = sb.ToString();
ScopeTiming.RecordScope("Stringy.ToString.sb.ToString", sw);
return str;
}
public static Stringy FromString(string str)
{
var dict = new Dictionary<string, string>();
foreach (string line in str.Split
(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
int colon = line.IndexOf(':');
string name = line.Substring(0, colon);
string value = line.Substring(colon + 1);
dict.Add(name, value);
}
double length = double.Parse(dict["length"]);
Particle[] particles =
dict["particles"]
.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => Particle.FromString(s))
.ToArray();
Stringy stringy = new Stringy(particles, length);
stringy.m_maxPos = double.Parse(dict["maxPos"]);
stringy.m_maxVel = double.Parse(dict["maxVel"]);
stringy.m_maxAcl = double.Parse(dict["maxAcl"]);
stringy.m_maxPunch = double.Parse(dict["maxPunch"]);
...
return stringy;
}
public Stringy Clone()
{
Stringy ret = new Stringy(m_particles, m_length);
ret.m_maxPos = m_maxPos;
ret.m_maxVel = m_maxVel;
ret.m_maxAcl = m_maxAcl;
ret.m_maxPunch = m_maxPunch;
...
return ret;
}
public
void Update
(
double startPosY,
double endPosY,
double elapsedTime,
double time,
double tension,
double damping,
double stringLength,
double stringConstant
)
{
double newStartWork = m_particles[0].SetPosY(startPosY, elapsedTime, time);
m_startWork += newStartWork;
if (newStartWork > m_maxStartWork)
m_maxStartWork = newStartWork;
double newEndWork = m_particles[m_particles.Length - 1].SetPosY
(endPosY, elapsedTime, time);
m_endWork += newEndWork;
if (newEndWork > m_maxEndWork)
m_maxEndWork = newEndWork;
for (int i = 0; i < m_particles.Length - 1; ++i)
{
double xGap = m_particles[i].x - m_particles[i + 1].x;
double yGap = m_particles[i].y - m_particles[i + 1].y;
double totalGap = Math.Sqrt(xGap * xGap + yGap * yGap);
m_particles[i].nextNeighborFactor = Math.Abs((1.0 / totalGap) * yGap);
}
for (int i = 1; i < m_particles.Length - 1; ++i)
{
double prevComponent = m_particles[i - 1].nextNeighborFactor;
if (m_particles[i - 1].y < m_particles[i].y)
prevComponent = -prevComponent;
double nextComponent = m_particles[i].nextNeighborFactor;
if (m_particles[i + 1].y < m_particles[i].y)
nextComponent = -nextComponent;
double newAcl = tension * (prevComponent + nextComponent)
- damping * m_particles[i].vel;
m_particles[i].punch = newAcl - m_particles[i].acl;
m_particles[i].acl = newAcl;
}
for (int i = 1; i < m_particles.Length - 1; ++i)
{
m_particles[i].vel += m_particles[i].acl * elapsedTime;
m_particles[i].y += m_particles[i].vel * elapsedTime;
}
...
}
...
}
The String(y) Class - C++
The C++ code is line for line identical to the C# code, except for... serialization. Our goal is to turn the string
into, well, a std::string
, as fast as possible. I got some great advice from Michaelgor: Use the std::to_chars
function to build the particles string.
void AppendToString(std::string& str, std::array<char, 1024 * 1024>& particlesBuffer) const
{
Stopwatch sw;
str += "particles:";
{
char* start = particlesBuffer.data();
char* end = start + particlesBuffer.size() - 1;
std::to_chars_result result;
for (const auto& p : m_particles)
{
result = std::to_chars(start, end, p.x);
if (result.ec != std::errc()) throw std::exception("to_chars error");
*result.ptr = ',';
start = result.ptr + 1;
result = std::to_chars(start, end, p.y);
if (result.ec != std::errc()) throw std::exception("to_chars error");
*result.ptr = ',';
start = result.ptr + 1;
result = std::to_chars(start, end, p.vel);
if (result.ec != std::errc()) throw std::exception("to_chars error");
*result.ptr = ',';
start = result.ptr + 1;
result = std::to_chars(start, end, p.acl);
if (result.ec != std::errc()) throw std::exception("to_chars error");
*result.ptr = ',';
start = result.ptr + 1;
result = std::to_chars(start, end, p.punch);
if (result.ec != std::errc()) throw std::exception("to_chars error");
*result.ptr = ',';
start = result.ptr + 1;
result = std::to_chars(start, end, p.nextNeighborFactor);
if (result.ec != std::errc()) throw std::exception("to_chars error");
*result.ptr = '|';
start = result.ptr + 1;
}
*start = '\0';
str += particlesBuffer.data();
}
str += ";";
ScopeTiming::GetObj().RecordScope("String.AppendToString.Particles", sw);
str += "length:" + std::to_string(m_length) + ";";
str += "maxPos:" + std::to_string(m_maxPos) + ";";
str += "maxVel:" + std::to_string(m_maxVel) + ";";
str += "maxAcl:" + std::to_string(m_maxAcl) + ";";
str += "maxPunch:" + std::to_string(m_maxPunch) + ";";
str += "maxPosIndex:" + std::to_string(m_maxPosIndex) + ";";
str += "maxVelIndex:" + std::to_string(m_maxVelIndex) + ";";
str += "maxAclIndex:" + std::to_string(m_maxAclIndex) + ";";
str += "maxPunchIndex:" + std::to_string(m_maxPunchIndex) + ";";
str += "startWork:" + std::to_string(m_startWork) + ";";
str += "endWork:" + std::to_string(m_endWork) + ";";
str += "maxStartWork:" + std::to_string(m_maxStartWork) + ";";
str += "maxEndWork:" + std::to_string(m_maxEndWork) + ";";
ScopeTiming::GetObj().RecordScope("String.AppendToString.TheRest", sw);
}
The Simulation Class - C#
...
public class Simulation
{
public const int cParticleCount = 1000;
public const double cStringConstant = 0.03164;
public const double cStringLength = 1.0;
public const double cOscillatorAmplitude = 0.001;
...
public void Update()
{
if (m_bPaused)
{
Thread.Sleep(200);
return;
}
int delayMs = 0;
{
lock (this)
{
delayMs = m_delayMs;
if (delayMs > 0)
{
if (m_delayMod > 0)
{
if ((m_simulationCycle % m_delayMod) != 0)
delayMs = 0;
}
}
}
}
if (delayMs >= 0)
Thread.Sleep(delayMs);
lock (this)
{
m_computeStopwatch.Restart();
++m_simulationCycle;
double startPos = 0.0;
if (m_bLeftEnabled)
{
foreach (double frequency in m_leftFrequencies)
{
startPos += GetOscillatorPosition(frequency,
cOscillatorAmplitude,
m_bJustPulse,
m_bJustHalfPulse,
m_outOfPhase,
m_time);
}
}
double endPos = 0.0;
if (m_bRightEnabled)
{
foreach (double frequency in m_rightFrequencies)
{
endPos += GetOscillatorPosition(frequency,
cOscillatorAmplitude,
m_bJustPulse,
m_bJustHalfPulse,
0.0,
m_time);
}
}
m_string.Update
(
startPos,
endPos,
m_timeSlice,
m_time,
m_tension,
m_damping,
cStringLength,
cStringConstant
);
if (Math.Abs(m_string.GetMaxPos()) > Math.Abs(m_maxPosString.GetMaxPos()))
{
m_maxPosString = m_string.Clone();
m_maxPosTime = m_time;
}
if (Math.Abs(m_string.GetMaxVel()) > Math.Abs(m_maxVelString.GetMaxVel()))
{
m_maxVelString = m_string.Clone();
m_maxVelTime = m_time;
}
if (Math.Abs(m_string.GetMaxAcl()) > Math.Abs(m_maxAclString.GetMaxAcl()))
{
m_maxAclString = m_string.Clone();
m_maxAclTime = m_time;
}
if (Math.Abs(m_string.GetMaxPunch()) > Math.Abs(m_maxPunchString.GetMaxPunch()))
{
m_maxPunchString = m_string.Clone();
m_maxPunchTime = m_time;
}
m_time += m_timeSlice;
ScopeTiming.RecordScope("Update", m_computeStopwatch);
}
}
public static double
GetOscillatorPosition
(
double frequency,
double amplitude,
bool bJustPulse,
bool bJustHalfPulse,
double outOfPhase,
double time
)
{
double radians = 2.0 * Math.PI * frequency * time;
radians -= outOfPhase * Math.PI;
if (radians < 0.0)
radians = 0.0;
if (bJustPulse && radians > 2.0 * Math.PI)
radians = 0.0;
else if (bJustHalfPulse && radians > 1.0 * Math.PI)
radians = 0.0;
double retVal = Math.Sin(radians);
retVal *= amplitude;
return retVal;
}
}
The C++ Update
function is line-for-line identical with the C#.
HttpListener and WebClient - Inter-process Communication Made Easy
I chose System.Net.HttpListener
and System.Net.WebClient
for the inter-process communication. They are simple and easy to use, and I highly recommend them for all your basic .NET client-server needs. Beats the hell out of socket / bind / listen / accept / recv / send...
There was some inclination to go with a binary format. I chose text because it was easy to implement, and the performance of running the simulation was much more important than updating the UI. The thinking is that you use the UI to kick off a simulation, then close the UI and let the server run for hours on end, then check in periodically with the UI. Serialization was fun to optimize, but it's not the high traffic path through the code in the common use case.
Performance Profiles
Here is the output from the ScopeTimings
of C# and C++. For each scope ("Output.StreamWriter
"), it lists...
- How many times called (hits)
- How much total time spent in the scope ("ms total")
- The average amount of time spent in the scope ("ms avg")
Simulation.AppendStrings -> 1984 hits - 21478 ms total -> 10.8256 ms avg
Simulation.Lock -> 1984 hits - 132564 ms total -> 66.81644 ms avg
Stringy.ToString.Particles -> 9920 hits - 19905 ms total -> 2.0065 ms avg
Output.StreamWriter -> 1984 hits - 16195 ms total -> 8.16298 ms avg
Output.ToString -> 1984 hits - 154508 ms total -> 77.87684 ms avg
Update -> 7768502 hits - 205235 ms total -> 0.02642 ms avg
Simulation.AppendStrings -> 3781 hits - 12345.393000 ms total -> 3.265113 ms avg
Simulation.Lock -> 3781 hits - 149573.137000 ms total -> 39.559148 ms
String.ToString.Particles -> 18905 hits - 12056.123000 ms total -> 0.637721 ms avg
Output.StreamWriter -> 3781 hits - 25779 ms total -> 6.81813 ms avg
Output.ToString -> 3781 hits - 165159 ms total -> 43.68125 ms avg
Update -> 26866573 hits - 412475.618075 ms total -> 0.015353 ms avg
C# runs 1.7X slower than C++ for the crucial Simulation Update function
Making C++ Fast
I already discussed using std::to_chars
for the serialization. I have made no effort to optimize the Simulator Update
function, I just let the compilers do their thing. It's surprising that identical basic math algorithm code is so much faster than C++. I'll take it!
Things were not always rosy for C++...
Initially, I had a single C++/CLI server project with the HttpListener
and all the C++ code burned in. This had pretty poor performance: .NET was 3X faster for building the Stringy particle string
s, and the all-important Simulator Update
function was pretty much identical performance.
Something had to give.
Michaelgor looked at the assembly, and was not impressed. This made us think that somehow C++/CLI code does not get rigorous C++ optimization.
So what I decided to do was pull the C++ class library out of the C++/CLI server into a Windows DLL, so the compiler can optimize the hell out of the C++. The DLL only had to expose what the server needed, so this was some small surgery":
extern "C"
{
__declspec(dllexport)
void StartSimulation();
__declspec(dllexport)
void ApplySimSettings(const char* settings);
__declspec(dllexport)
const char* GetSimState();
__declspec(dllexport)
const char* GetTimingSummary();
}
#include...
Simulation* sim = nullptr;
auto particlesBuffer = new std::array<char, 1024 * 1024>();
void StartSimulation()
{
sim = new Simulation();
ScopeTiming::GetObj().Init(true);
}
void ApplySimSettings(const char* settings)
{
sim->ApplySettings(settings);
}
std::string simState;
const char* GetSimState()
{
sim->ToString(simState, *particlesBuffer);
return simState.c_str();
}
std::string timingSummary;
const char* GetTimingSummary()
{
timingSummary = ScopeTiming::GetObj().GetSummary();
return timingSummary.c_str();
}
I tried sticking with a C++/CLI server, but there was a compiler error that I could not get around, StreamReader
/StreamWriter
classes being ambiguous symbols. So I chose to create a C# driver program with DLL calls:
using...
namespace...
Program...
[DllImport("sealib.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void StartSimulation();
[DllImport("sealib.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void ApplySimSettings(string settings);
[DllImport("sealib.dll", CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr GetSimState();
[DllImport("sealib.dll", CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr GetTimingSummary();
static string GetString(IntPtr ptr)
{
return Marshal.PtrToStringAnsi(ptr);
}
static void RunStats()
{
while (true)
{
Thread.Sleep(3 * 1000);
string seaTiming = GetString(GetTimingSummary());
string svrTiming = ScopeTiming.Summary;
string timing = "";
if (seaTiming != "")
timing += seaTiming;
if (svrTiming != "")
{
if (timing != "")
timing += '\n';
timing += svrTiming;
}
if (timing != "")
timing += '\n';
Console.WriteLine(timing);
}
}
static void Main(string[] args)
{
ScopeTiming.Init(true);
Console.Write("Setting up simulation...");
StartSimulation();
Console.WriteLine("done!");
Console.Write("Setting up server...");
HttpListener listener = new HttpListener();
listener.Prefixes.Add($"http://localhost:9914/");
listener.Start();
Console.WriteLine("done!");
new Thread(RunStats).Start();
Stopwatch sw = new Stopwatch();
while (true)
{
var ctxt = listener.GetContext();
if (ctxt.Request.HttpMethod == "GET")
{
sw.Restart();
string state = GetString(GetSimState());
ScopeTiming.RecordScope("Output.ToString", sw);
using (StreamWriter writer = new StreamWriter(ctxt.Response.OutputStream))
writer.Write(state);
ScopeTiming.RecordScope("Output.StreamWriter", sw);
}
else
{
sw.Restart();
string settings;
using (StreamReader reader = new StreamReader(ctxt.Request.InputStream))
settings = reader.ReadToEnd();
ScopeTiming.RecordScope("Settings.StreamReader", sw);
ApplySimSettings(settings);
ScopeTiming.RecordScope("Settings.Apply", sw);
}
ctxt.Response.OutputStream.Close();
}
}
Conclusion and Points of Interest
This has been a great game of software development, tuning, measurement, and optimization. With my initial implementation, it seemed there was no benefit to using C++ vs. C# for this application. But with optimization and rearrangement of the C++ code, C++ is clearly the way to go for the simulation, using C# for inter-process communication and user interface.
The software is open source on GitHub. Perhaps a Linux server with React frontend?
Back to mad science...
You may be wondering where the name StringShear
came from. If you're game, try picking some frequencies to cause a particle to fly off, or to see something interesting, and share what you find. Such repeatable, noteworthy findings could be of scientific value. Well, at least it's fun to play with.
And have at the code, explore performance with more timing, try speeding things up.
Enjoy!
History
- 6th September, 2021: Initial version
- 12th September, 2021: Updated article based on feedback from the community, and significant software improvement